bidi2pdf 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -1
- data/README.md +168 -55
- data/docker/Dockerfile +11 -3
- data/docker/Dockerfile.chromedriver +4 -2
- data/docker/Dockerfile.slim +75 -0
- data/lib/bidi2pdf/bidi/browser_tab.rb +47 -7
- data/lib/bidi2pdf/bidi/commands/set_tab_cookie.rb +9 -1
- data/lib/bidi2pdf/bidi/logger_events.rb +86 -0
- data/lib/bidi2pdf/bidi/network_event.rb +25 -7
- data/lib/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rb +109 -0
- data/lib/bidi2pdf/bidi/network_event_formatters/network_event_formatter_utils.rb +53 -0
- data/lib/bidi2pdf/bidi/network_event_formatters/network_event_html_formatter.rb +125 -0
- data/lib/bidi2pdf/bidi/network_event_formatters.rb +11 -0
- data/lib/bidi2pdf/bidi/network_events.rb +24 -5
- data/lib/bidi2pdf/bidi/session.rb +1 -1
- data/lib/bidi2pdf/cli.rb +28 -5
- data/lib/bidi2pdf/dsl.rb +45 -0
- data/lib/bidi2pdf/launcher.rb +7 -3
- data/lib/bidi2pdf/session_runner.rb +21 -4
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +7 -2
- metadata +9 -2
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "network_event"
|
4
|
+
|
5
|
+
module Bidi2pdf
|
6
|
+
module Bidi
|
7
|
+
class LoggerEvents
|
8
|
+
attr_reader :context_id
|
9
|
+
|
10
|
+
def initialize(context_id)
|
11
|
+
@context_id = context_id
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_event(data)
|
15
|
+
event = data["params"]
|
16
|
+
method = data["method"]
|
17
|
+
|
18
|
+
if event.dig("source", "context") == context_id
|
19
|
+
handle_response(method, event)
|
20
|
+
else
|
21
|
+
Bidi2pdf.logger.debug "Ignoring Log event: #{method}, context_id: #{context_id}, params: #{event}"
|
22
|
+
end
|
23
|
+
rescue StandardError => e
|
24
|
+
Bidi2pdf.logger.error "Error handling Log event: #{e.message}\n#{e.backtrace&.join("\n")}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def handle_response(_method, event)
|
28
|
+
level = resolve_log_level(event["level"])
|
29
|
+
text = event["text"]
|
30
|
+
args = event["args"] || []
|
31
|
+
stack_trace = event["stackTrace"]
|
32
|
+
timestamp = format_timestamp(event["timestamp"])
|
33
|
+
prefix = log_prefix(timestamp)
|
34
|
+
|
35
|
+
log_message(level, prefix, text)
|
36
|
+
log_args(prefix, args)
|
37
|
+
log_stack_trace(prefix, stack_trace) if stack_trace && level == :error
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def log_message(level, prefix, text)
|
43
|
+
return unless text
|
44
|
+
|
45
|
+
Bidi2pdf.logger.send(level, "#{prefix} #{text}")
|
46
|
+
end
|
47
|
+
|
48
|
+
def log_args(prefix, args)
|
49
|
+
return if args.empty?
|
50
|
+
|
51
|
+
Bidi2pdf.logger.debug("#{prefix} Args: #{args.inspect}")
|
52
|
+
end
|
53
|
+
|
54
|
+
def log_stack_trace(prefix, trace)
|
55
|
+
formatted_trace = format_stack_trace(trace)
|
56
|
+
Bidi2pdf.logger.error("#{prefix} Stack trace captured:\n#{formatted_trace}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def format_timestamp(timestamp)
|
60
|
+
return "N/A" unless timestamp
|
61
|
+
|
62
|
+
Time.at(timestamp.to_f / 1000).utc.strftime("%Y-%m-%d %H:%M:%S.%L UTC")
|
63
|
+
end
|
64
|
+
|
65
|
+
def format_stack_trace(trace)
|
66
|
+
trace["callFrames"].each_with_index.map do |frame, index|
|
67
|
+
function = frame["functionName"].to_s.empty? ? "(anonymous)" : frame["functionName"]
|
68
|
+
"##{index} #{function} at #{frame["url"]}:#{frame["lineNumber"]}:#{frame["columnNumber"]}"
|
69
|
+
end.join("\n")
|
70
|
+
end
|
71
|
+
|
72
|
+
def resolve_log_level(js_level)
|
73
|
+
case js_level
|
74
|
+
when "info", "warn", "error"
|
75
|
+
js_level.to_sym
|
76
|
+
else
|
77
|
+
:debug
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def log_prefix(timestamp)
|
82
|
+
"[#{timestamp}][Browser Console Log]"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -3,7 +3,8 @@
|
|
3
3
|
module Bidi2pdf
|
4
4
|
module Bidi
|
5
5
|
class NetworkEvent
|
6
|
-
attr_reader :id, :url, :state, :start_timestamp, :end_timestamp, :timing
|
6
|
+
attr_reader :id, :url, :state, :start_timestamp, :end_timestamp, :timing, :http_status_code,
|
7
|
+
:http_method, :bytes_received
|
7
8
|
|
8
9
|
STATE_MAP = {
|
9
10
|
"network.responseStarted" => "started",
|
@@ -11,18 +12,22 @@ module Bidi2pdf
|
|
11
12
|
"network.fetchError" => "error"
|
12
13
|
}.freeze
|
13
14
|
|
14
|
-
def initialize(id:, url:, timestamp:, timing:, state:)
|
15
|
+
def initialize(id:, url:, timestamp:, timing:, state:, http_status_code: nil, http_method: nil)
|
15
16
|
@id = id
|
16
17
|
@url = url
|
17
18
|
@start_timestamp = timestamp
|
18
19
|
@timing = timing
|
19
20
|
@state = map_state(state)
|
21
|
+
@http_status_code = http_status_code
|
22
|
+
@http_method = http_method
|
20
23
|
end
|
21
24
|
|
22
|
-
def update_state(new_state, timestamp: nil, timing: nil)
|
25
|
+
def update_state(new_state, timestamp: nil, timing: nil, http_status_code: nil, bytes_received: nil)
|
23
26
|
@state = map_state(new_state)
|
24
27
|
@end_timestamp = timestamp if timestamp
|
25
28
|
@timing = timing if timing
|
29
|
+
@http_status_code = http_status_code if http_status_code
|
30
|
+
@bytes_received = bytes_received if bytes_received
|
26
31
|
end
|
27
32
|
|
28
33
|
def map_state(state)
|
@@ -44,10 +49,23 @@ module Bidi2pdf
|
|
44
49
|
def in_progress? = state == "started"
|
45
50
|
|
46
51
|
def to_s
|
47
|
-
took_str = duration_seconds ? "
|
48
|
-
|
49
|
-
|
50
|
-
|
52
|
+
took_str = duration_seconds ? "#{duration_seconds.round(2)} sec" : "in progress"
|
53
|
+
http_status = @http_status_code ? "HTTP #{@http_status_code}" : "HTTP (N/A)"
|
54
|
+
start_str = format_timestamp(@start_timestamp) || "N/A"
|
55
|
+
end_str = format_timestamp(@end_timestamp) || "N/A"
|
56
|
+
method_str = @http_method || "N/A"
|
57
|
+
bytes_str = @bytes_received ? "#{@bytes_received} bytes" : "0 bytes"
|
58
|
+
|
59
|
+
"#<NetworkEvent " \
|
60
|
+
"id=#{@id.inspect}, " \
|
61
|
+
"method=#{method_str.inspect}, " \
|
62
|
+
"url=#{@url.inspect}, " \
|
63
|
+
"state=#{@state.inspect}, " \
|
64
|
+
"#{http_status}, " \
|
65
|
+
"bytes_received=#{bytes_str}, " \
|
66
|
+
"start=#{start_str}, " \
|
67
|
+
"end=#{end_str}, " \
|
68
|
+
"duration=#{took_str}>"
|
51
69
|
end
|
52
70
|
end
|
53
71
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module Bidi
|
5
|
+
module NetworkEventFormatters
|
6
|
+
# rubocop: disable Metrics/AbcSize
|
7
|
+
class NetworkEventConsoleFormatter
|
8
|
+
include NetworkEventFormatterUtils
|
9
|
+
|
10
|
+
attr_reader :color_enabled
|
11
|
+
|
12
|
+
# ANSI styles
|
13
|
+
RESET = "\e[0m"
|
14
|
+
BOLD = "\e[1m"
|
15
|
+
DIM = "\e[2m"
|
16
|
+
RED = "\e[31m"
|
17
|
+
GREEN = "\e[32m"
|
18
|
+
YELLOW = "\e[33m"
|
19
|
+
CYAN = "\e[36m"
|
20
|
+
GRAY = "\e[90m"
|
21
|
+
|
22
|
+
def initialize(color: true)
|
23
|
+
@color_enabled = color
|
24
|
+
end
|
25
|
+
|
26
|
+
def log(events)
|
27
|
+
events.each { |event| pretty_log(event).each_line { |line| Bidi2pdf.network_events_logger.info(line.chomp) } }
|
28
|
+
end
|
29
|
+
|
30
|
+
def pretty_log(event)
|
31
|
+
status = event.http_status_code ? "HTTP #{event.http_status_code}" : "pending"
|
32
|
+
status_color = color_for_status(event.http_status_code)
|
33
|
+
start = event.format_timestamp(event.start_timestamp)
|
34
|
+
finish = event.end_timestamp ? event.format_timestamp(event.end_timestamp) : dim("...")
|
35
|
+
duration = event.duration_seconds ? cyan("#{event.duration_seconds}s") : dim("in progress")
|
36
|
+
timing_details = format_timing(event)
|
37
|
+
bytes = event.bytes_received ? format_bytes(event.bytes_received) : dim("N/A")
|
38
|
+
displayed_url = shorten_url(event.url)
|
39
|
+
|
40
|
+
<<~LOG.strip
|
41
|
+
#{bold("┌─ Network Event ──────────────────────────────────────")}
|
42
|
+
#{bold("│ Request: ")}#{event.http_method || "?"} #{displayed_url}#{" "}
|
43
|
+
#{bold("│ State: ")}#{event.state}
|
44
|
+
#{bold("│ Status: ")}#{status_color}#{status}#{reset}
|
45
|
+
#{bold("│ Started: ")}#{start}
|
46
|
+
#{bold("│ Ended: ")}#{finish}
|
47
|
+
#{bold("│ Duration:")} #{duration}
|
48
|
+
#{bold("│ Received:")} #{bytes}
|
49
|
+
#{timing_details}
|
50
|
+
#{bold("└──────────────────────────────────────────────────────")}
|
51
|
+
LOG
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def format_timing(event)
|
57
|
+
return "" unless event.timing.is_a?(Hash)
|
58
|
+
|
59
|
+
keys = %w[
|
60
|
+
requestTime proxyStart proxyEnd dnsStart dnsEnd connectStart connectEnd
|
61
|
+
sslStart sslEnd workerStart workerReady sendStart sendEnd receiveHeadersEnd
|
62
|
+
]
|
63
|
+
|
64
|
+
visible = keys.map do |key|
|
65
|
+
next unless event.timing[key]
|
66
|
+
|
67
|
+
label = key.gsub(/([A-Z])/, ' \1').capitalize
|
68
|
+
"#{dim("│")} #{label.ljust(20)}: #{event.timing[key].round(2)} ms#{reset}"
|
69
|
+
end.compact
|
70
|
+
|
71
|
+
return "" if visible.empty?
|
72
|
+
|
73
|
+
[dim("│").to_s, dim("│ Timing Phases:").to_s].concat(visible).join("\n")
|
74
|
+
end
|
75
|
+
|
76
|
+
# === Color Helpers ===
|
77
|
+
|
78
|
+
def color_for_status(code)
|
79
|
+
return gray unless code
|
80
|
+
|
81
|
+
case code.to_i
|
82
|
+
when 200..299 then green
|
83
|
+
when 300..499 then yellow
|
84
|
+
when 500..599 then red
|
85
|
+
else gray
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def bold(str) = color_enabled ? "#{BOLD}#{str}#{RESET}" : str
|
90
|
+
|
91
|
+
def dim(str) = color_enabled ? "#{DIM}#{str}#{RESET}" : str
|
92
|
+
|
93
|
+
def green(str = "") = color_enabled ? "#{GREEN}#{str}" : str
|
94
|
+
|
95
|
+
def yellow(str = "") = color_enabled ? "#{YELLOW}#{str}" : str
|
96
|
+
|
97
|
+
def red(str = "") = color_enabled ? "#{RED}#{str}" : str
|
98
|
+
|
99
|
+
def cyan(str = "") = color_enabled ? "#{CYAN}#{str}" : str
|
100
|
+
|
101
|
+
def gray(str = "") = color_enabled ? "#{GRAY}#{str}" : str
|
102
|
+
|
103
|
+
def reset = color_enabled ? RESET : ""
|
104
|
+
|
105
|
+
# rubocop: enable Metrics/AbcSize
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cgi"
|
4
|
+
|
5
|
+
module Bidi2pdf
|
6
|
+
module Bidi
|
7
|
+
module NetworkEventFormatters
|
8
|
+
module NetworkEventFormatterUtils
|
9
|
+
def format_bytes(size)
|
10
|
+
return "N/A" unless size.is_a?(Numeric)
|
11
|
+
|
12
|
+
units = %w[B KB MB GB TB]
|
13
|
+
idx = 0
|
14
|
+
while size >= 1024 && idx < units.size - 1
|
15
|
+
size /= 1024.0
|
16
|
+
idx += 1
|
17
|
+
end
|
18
|
+
format("%<size>.2f %<unit>s", size: size, unit: units[idx])
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_timing(event)
|
22
|
+
return [] unless event.timing.is_a?(Hash)
|
23
|
+
|
24
|
+
keys = %w[
|
25
|
+
requestTime proxyStart proxyEnd dnsStart dnsEnd connectStart connectEnd
|
26
|
+
sslStart sslEnd workerStart workerReady sendStart sendEnd receiveHeadersEnd
|
27
|
+
]
|
28
|
+
|
29
|
+
keys.filter_map do |key|
|
30
|
+
next unless event.timing[key]
|
31
|
+
|
32
|
+
label = key.gsub(/([A-Z])/, ' \1').capitalize
|
33
|
+
{ label: label, key: key, ms: event.timing[key].round(2) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def format_timestamp(timestamp)
|
38
|
+
return "N/A" unless timestamp
|
39
|
+
|
40
|
+
Time.at(timestamp.to_f / 1000).utc.strftime("%Y-%m-%d %H:%M:%S.%L UTC")
|
41
|
+
end
|
42
|
+
|
43
|
+
def shorten_url(url)
|
44
|
+
sanitized_url = CGI.escapeHTML(url)
|
45
|
+
|
46
|
+
return sanitized_url unless sanitized_url.start_with?("data:text/html")
|
47
|
+
|
48
|
+
"data:text/html,..."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module Bidi
|
5
|
+
module NetworkEventFormatters
|
6
|
+
class NetworkEventHtmlFormatter
|
7
|
+
include NetworkEventFormatterUtils
|
8
|
+
|
9
|
+
def render(events)
|
10
|
+
return unless Bidi2pdf.network_events_logger.info?
|
11
|
+
|
12
|
+
<<~HTML
|
13
|
+
<!DOCTYPE html>
|
14
|
+
<html lang="en" data-bs-theme="light">
|
15
|
+
<head>
|
16
|
+
<meta charset="UTF-8">
|
17
|
+
<title>Network Events</title>
|
18
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
19
|
+
<style>
|
20
|
+
body { font-family: monospace; padding: 2rem; }
|
21
|
+
.event { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); padding: 1rem; margin-bottom: 2rem; border-radius: .5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
22
|
+
.bar-container { position: relative; height: 20px; background: var(--bs-secondary-bg); border-radius: 4px; }
|
23
|
+
.bar { position: absolute; top: 0; height: 100%; background-color: #0d6efd; opacity: 0.8; }
|
24
|
+
.toc a { text-decoration: none; display: block; margin-bottom: 0.5rem; }
|
25
|
+
|
26
|
+
@media print {
|
27
|
+
.no-break {
|
28
|
+
page-break-inside: avoid;
|
29
|
+
}
|
30
|
+
#{" "}
|
31
|
+
#{" "}
|
32
|
+
.form-select, .form-label, #theme-select {
|
33
|
+
display: none !important; /* Hide theme selector when printing */
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
.event {
|
38
|
+
word-break: break-word;
|
39
|
+
overflow-wrap: anywhere;
|
40
|
+
}
|
41
|
+
</style>
|
42
|
+
<script>
|
43
|
+
function toggleTheme(value) {
|
44
|
+
document.documentElement.setAttribute('data-bs-theme', value);
|
45
|
+
}
|
46
|
+
</script>
|
47
|
+
</head>
|
48
|
+
<body>
|
49
|
+
<h1>Network Events</h1>
|
50
|
+
<div class="mb-4">
|
51
|
+
<label for="theme-select" class="form-label">Theme:</label>
|
52
|
+
<select id="theme-select" class="form-select w-auto d-inline-block" onchange="toggleTheme(this.value)">
|
53
|
+
<option value="light">Light</option>
|
54
|
+
<option value="dark">Dark</option>
|
55
|
+
</select>
|
56
|
+
</div>
|
57
|
+
|
58
|
+
<h2>Index</h2>
|
59
|
+
<div class="toc mb-4">
|
60
|
+
#{events.map.with_index { |e, i| toc_entry(e, i) }.join("\n")}
|
61
|
+
</div>
|
62
|
+
|
63
|
+
#{events.map.with_index { |e, i| render_event(e, i) }.join("\n")}
|
64
|
+
</body>
|
65
|
+
</html>
|
66
|
+
HTML
|
67
|
+
end
|
68
|
+
|
69
|
+
def toc_entry(event, index)
|
70
|
+
"<a href=\"#event-#{index}\">[#{index + 1}] #{event.http_method} #{event.url}</a>"
|
71
|
+
end
|
72
|
+
|
73
|
+
# rubocop: disable Metrics/AbcSize
|
74
|
+
def render_event(event, index)
|
75
|
+
timing = parse_timing(event)
|
76
|
+
duration = event.duration_seconds || 0
|
77
|
+
duration_str = event.in_progress? ? "in progress" : "#{duration}s"
|
78
|
+
status = event.http_status_code || "?"
|
79
|
+
method = event.http_method || "?"
|
80
|
+
start = format_timestamp(event.start_timestamp)
|
81
|
+
finish = event.end_timestamp ? format_timestamp(event.end_timestamp) : "..."
|
82
|
+
bytes = event.bytes_received ? format_bytes(event.bytes_received) : "N/A"
|
83
|
+
bars = render_timing_bars(timing)
|
84
|
+
displayed_url = shorten_url(event.url)
|
85
|
+
|
86
|
+
<<~HTML
|
87
|
+
<div class="event no-break" id="event-#{index}">
|
88
|
+
<div><strong>Request:</strong> #{method} #{displayed_url}</div>
|
89
|
+
<div><strong>Status:</strong> HTTP #{status}</div>
|
90
|
+
<div><strong>State:</strong> #{event.state}</div>
|
91
|
+
<div><strong>Start:</strong> #{start}</div>
|
92
|
+
<div><strong>End:</strong> #{finish}</div>
|
93
|
+
<div><strong>Duration:</strong> #{duration_str}</div>
|
94
|
+
<div><strong>Received:</strong> #{bytes}</div>
|
95
|
+
#{bars}
|
96
|
+
</div>
|
97
|
+
HTML
|
98
|
+
end
|
99
|
+
|
100
|
+
# rubocop: enable Metrics/AbcSize
|
101
|
+
|
102
|
+
def render_timing_bars(timing)
|
103
|
+
return "" if timing.empty?
|
104
|
+
|
105
|
+
max_ms = timing.map { |t| t[:ms] }.max
|
106
|
+
scale = max_ms.zero? ? 0 : 100.0 / max_ms
|
107
|
+
|
108
|
+
bars = timing.map do |t|
|
109
|
+
width = (t[:ms] * scale).clamp(1, 100).round(2)
|
110
|
+
<<~HTML
|
111
|
+
<div>
|
112
|
+
<small>#{t[:label]} (#{t[:ms]} ms)</small>
|
113
|
+
<div class="bar-container mb-2">
|
114
|
+
<div class="bar" style="width: #{width}%"></div>
|
115
|
+
</div>
|
116
|
+
</div>
|
117
|
+
HTML
|
118
|
+
end
|
119
|
+
|
120
|
+
"<div class=\"mt-3\"><strong>Timing Waterfall</strong>#{bars.join}</div>"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module Bidi
|
5
|
+
module NetworkEventFormatters
|
6
|
+
require_relative "network_event_formatters/network_event_formatter_utils"
|
7
|
+
require_relative "network_event_formatters/network_event_console_formatter"
|
8
|
+
require_relative "network_event_formatters/network_event_html_formatter"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -1,15 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "network_event"
|
4
|
+
require_relative "network_event_formatters"
|
4
5
|
|
5
6
|
module Bidi2pdf
|
6
7
|
module Bidi
|
7
8
|
class NetworkEvents
|
8
|
-
attr_reader :context_id, :events
|
9
|
+
attr_reader :context_id, :events, :network_event_formatter
|
9
10
|
|
10
11
|
def initialize(context_id)
|
11
12
|
@context_id = context_id
|
12
13
|
@events = {}
|
14
|
+
@network_event_formatter = NetworkEventFormatters::NetworkEventConsoleFormatter.new
|
13
15
|
end
|
14
16
|
|
15
17
|
def handle_event(data)
|
@@ -30,23 +32,28 @@ module Bidi2pdf
|
|
30
32
|
return unless event && event["request"]
|
31
33
|
|
32
34
|
request = event["request"]
|
35
|
+
response = event["response"]
|
36
|
+
http_status_code = response&.dig("status")
|
37
|
+
bytes_received = response&.dig("bytesReceived")
|
33
38
|
|
34
39
|
id = request["request"]
|
35
40
|
url = request["url"]
|
36
41
|
timing = request["timings"]
|
42
|
+
http_method = request["method"]
|
37
43
|
|
38
44
|
timestamp = event["timestamp"]
|
39
45
|
|
40
|
-
if method == "network.
|
46
|
+
if method == "network.beforeRequestSent"
|
41
47
|
events[id] ||= NetworkEvent.new(
|
42
48
|
id: id,
|
43
49
|
url: url,
|
44
50
|
timestamp: timestamp,
|
45
51
|
timing: timing,
|
46
|
-
state: method
|
52
|
+
state: method,
|
53
|
+
http_method: http_method
|
47
54
|
)
|
48
55
|
elsif events.key?(id)
|
49
|
-
events[id].update_state(method, timestamp: timestamp, timing: timing)
|
56
|
+
events[id].update_state(method, timestamp: timestamp, timing: timing, http_status_code: http_status_code, bytes_received: bytes_received)
|
50
57
|
else
|
51
58
|
Bidi2pdf.logger.warn "Received response for unknown request ID: #{id}, URL: #{url}"
|
52
59
|
end
|
@@ -58,7 +65,19 @@ module Bidi2pdf
|
|
58
65
|
events.values.sort_by(&:start_timestamp)
|
59
66
|
end
|
60
67
|
|
61
|
-
def
|
68
|
+
def log_network_traffic(format: :console)
|
69
|
+
format = format.to_sym
|
70
|
+
|
71
|
+
if format == :console
|
72
|
+
NetworkEventFormatters::NetworkEventConsoleFormatter.new.log all_events
|
73
|
+
elsif format == :html
|
74
|
+
NetworkEventFormatters::NetworkEventHtmlFormatter.new.render(all_events)
|
75
|
+
else
|
76
|
+
raise ArgumentError, "Unknown network event format: #{format}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def wait_until_network_idle(timeout: 10, poll_interval: 0.1)
|
62
81
|
start_time = Time.now
|
63
82
|
|
64
83
|
loop do
|
@@ -10,7 +10,7 @@ require_relative "user_context"
|
|
10
10
|
module Bidi2pdf
|
11
11
|
module Bidi
|
12
12
|
class Session
|
13
|
-
SUBSCRIBE_EVENTS = %w[
|
13
|
+
SUBSCRIBE_EVENTS = %w[script].freeze
|
14
14
|
DEFAULT_CHROME_ARGS = %w[--disable-gpu --disable-popup-blocking --disable-hang-monitor].freeze
|
15
15
|
|
16
16
|
attr_reader :session_uri, :started, :chrome_args
|
data/lib/bidi2pdf/cli.rb
CHANGED
@@ -31,6 +31,10 @@ module Bidi2pdf
|
|
31
31
|
USAGE
|
32
32
|
|
33
33
|
option :url, desc: "The URL to render"
|
34
|
+
# of course, it's possible to render a local file via: --url file:///path/to/the/file.html
|
35
|
+
# but this should showcase the scenario that you render a string within ruby as pdf without the need
|
36
|
+
# to store it on disc
|
37
|
+
option :html_file, desc: "The local HTML file to render"
|
34
38
|
option :output, default: "output.pdf", desc: "Filename for the output PDF", aliases: "-o"
|
35
39
|
option :cookie, type: :array, default: [], banner: "name=value", desc: "One or more cookies", aliases: "-C"
|
36
40
|
option :header, type: :array, default: [], banner: "name=value", desc: "One or more custom headers", aliases: "-H"
|
@@ -47,6 +51,12 @@ module Bidi2pdf
|
|
47
51
|
option :log_level,
|
48
52
|
type: :string,
|
49
53
|
default: "info", enum: %w[debug info warn error fatal unknown], desc: "Set log level"
|
54
|
+
option :log_network_traffic, type: :boolean, default: false, desc: "Log network traffic", aliases: "-n"
|
55
|
+
option :network_log_format,
|
56
|
+
type: :string,
|
57
|
+
default: "console",
|
58
|
+
enum: %w[console pdf],
|
59
|
+
desc: "Choose network log format: console or pdf", aliases: "-f"
|
50
60
|
|
51
61
|
option :background, type: :boolean, default: true, desc: "Print background graphics"
|
52
62
|
option :margin_top, type: :numeric, default: 1.0, desc: "Top margin in inches"
|
@@ -60,10 +70,16 @@ module Bidi2pdf
|
|
60
70
|
option :scale, type: :numeric, default: 1.0, desc: "Scale between 0.1 and 2.0"
|
61
71
|
option :shrink_to_fit, type: :boolean, default: true, desc: "Shrink content to fit page"
|
62
72
|
|
73
|
+
class << self
|
74
|
+
def exit_on_failure?
|
75
|
+
true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
63
79
|
def render
|
64
80
|
load_config
|
65
81
|
|
66
|
-
validate_required_options!
|
82
|
+
validate_required_options!
|
67
83
|
|
68
84
|
configure
|
69
85
|
|
@@ -115,9 +131,11 @@ module Bidi2pdf
|
|
115
131
|
YAML.load_file(options[:config]).transform_keys(&:to_sym)
|
116
132
|
end
|
117
133
|
|
118
|
-
def validate_required_options!
|
119
|
-
|
120
|
-
raise Thor::Error, "Missing required option
|
134
|
+
def validate_required_options!
|
135
|
+
if merged_options[:url].nil? && merged_options[:html_file].nil?
|
136
|
+
raise Thor::Error, "Missing required option --url or --html_file needs to be specified"
|
137
|
+
elsif merged_options[:html_file] && !File.readable?(merged_options[:html_file])
|
138
|
+
raise Thor::Error, "HTML file '#{merged_options[:html_file]}' not found or not readable"
|
121
139
|
end
|
122
140
|
end
|
123
141
|
|
@@ -182,6 +200,7 @@ module Bidi2pdf
|
|
182
200
|
|
183
201
|
Bidi2pdf::Launcher.new(
|
184
202
|
url: merged_options[:url],
|
203
|
+
inputfile: merged_options[:hmtl_file],
|
185
204
|
output: merged_options[:output],
|
186
205
|
cookies: parse_key_values(merged_options[:cookie]),
|
187
206
|
headers: parse_key_values(merged_options[:header]),
|
@@ -191,7 +210,8 @@ module Bidi2pdf
|
|
191
210
|
headless: merged_options[:headless],
|
192
211
|
wait_window_loaded: merged_options[:wait_window_loaded],
|
193
212
|
wait_network_idle: merged_options[:wait_network_idle],
|
194
|
-
print_options: print_options
|
213
|
+
print_options: print_options,
|
214
|
+
network_log_format: merged_options[:network_log_format]
|
195
215
|
)
|
196
216
|
end
|
197
217
|
end
|
@@ -201,6 +221,9 @@ module Bidi2pdf
|
|
201
221
|
def configure
|
202
222
|
Bidi2pdf.configure do |config|
|
203
223
|
config.logger.level = log_level
|
224
|
+
|
225
|
+
config.network_events_logger.level = Logger::INFO if merged_options[:log_network_traffic]
|
226
|
+
|
204
227
|
config.default_timeout = merged_options[:default_timeout]
|
205
228
|
|
206
229
|
Chromedriver::Binary.configure do |c|
|
data/lib/bidi2pdf/dsl.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bidi2pdf"
|
4
|
+
|
5
|
+
module Bidi2pdf
|
6
|
+
module DSL
|
7
|
+
# rubocop: disable Metrics/AbcSize
|
8
|
+
def self.with_tab(remote_browser_url: nil, port: 0, headless: true, chrome_args: Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS.dup)
|
9
|
+
manager = nil
|
10
|
+
session = nil
|
11
|
+
tab = nil
|
12
|
+
|
13
|
+
begin
|
14
|
+
session = if remote_browser_url
|
15
|
+
Bidi2pdf::Bidi::Session.new(
|
16
|
+
session_url: remote_browser_url,
|
17
|
+
headless: true, # remote is always headless
|
18
|
+
chrome_args: chrome_args
|
19
|
+
)
|
20
|
+
else
|
21
|
+
manager = Bidi2pdf::ChromedriverManager.new(port: port, headless: headless)
|
22
|
+
manager.start
|
23
|
+
manager.session
|
24
|
+
end
|
25
|
+
|
26
|
+
session.start
|
27
|
+
session.client.on_close { Bidi2pdf.logger.info "WebSocket session closed" }
|
28
|
+
|
29
|
+
browser = session.browser
|
30
|
+
context = browser.create_user_context
|
31
|
+
window = context.create_browser_window
|
32
|
+
tab = window.create_browser_tab
|
33
|
+
|
34
|
+
yield(tab)
|
35
|
+
ensure
|
36
|
+
tab&.close
|
37
|
+
window&.close
|
38
|
+
session&.close
|
39
|
+
manager&.stop
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# rubocop: enable Metrics/AbcSize
|
44
|
+
end
|
45
|
+
end
|