bidi2pdf 0.1.5 → 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 +32 -0
- data/README.md +33 -10
- data/docker/Dockerfile +11 -3
- data/docker/Dockerfile.chromedriver +4 -2
- data/docker/Dockerfile.slim +75 -0
- data/lib/bidi2pdf/bidi/browser_tab.rb +33 -7
- 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 +17 -1
- data/lib/bidi2pdf/launcher.rb +4 -2
- data/lib/bidi2pdf/session_runner.rb +14 -4
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +3 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 541e9193b3285383b2a82065b173af87c68edeb08ec157125761f44285118dff
|
4
|
+
data.tar.gz: ab12e33ad21e8377ef964aa12d0d9b0ba757ee941c217febe6d46fb1cc381d5e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 91be15b3310098c7f16153331fa0a27a8b869dfebc9f3720a7da26d2c59b8444f266e21e93d74891b423e50d8223b01b2198f230753d38ad523b8c846c12150f
|
7
|
+
data.tar.gz: 504516ecc1eac40e6c6e159c5f14aafdeb9d23bc587fcf6914ab60d8676a930a7866d6d0c07998398359c402780584f1e5b317c274dd6f3dba0ba50c1a73cbe6
|
data/CHANGELOG.md
CHANGED
@@ -6,10 +6,34 @@ All notable changes to this project will be documented in this file.
|
|
6
6
|
|
7
7
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
8
8
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
9
|
+
|
9
10
|
[unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.5..HEAD
|
10
11
|
|
11
12
|
<!-- generated by git-cliff end -->
|
12
13
|
|
14
|
+
## [0.1.6]
|
15
|
+
|
16
|
+
### ⚠️ Breaking Changes
|
17
|
+
|
18
|
+
- Rename view_html_page to render_html by @dieter-medium
|
19
|
+
- Rename wait_until_all_finished to wait_until_network_idle by @dieter-medium
|
20
|
+
|
21
|
+
### 📝 Docs
|
22
|
+
|
23
|
+
- Update Docker instructions in README by @dieter-medium
|
24
|
+
|
25
|
+
### 🔄 Changed
|
26
|
+
|
27
|
+
- Add details on network logging and console capture by @dieter-medium
|
28
|
+
|
29
|
+
### 🚀 Added
|
30
|
+
|
31
|
+
- Add PDF network log formatting and customizable outputs by @dieter-medium
|
32
|
+
- Add option to log network traffic and handle failures within the cli command by @dieter-medium
|
33
|
+
- Add structured network traffic logging by @dieter-medium
|
34
|
+
- Add slim variant Dockerfile and build matrix for CI by @dieter-medium
|
35
|
+
- Add official image at [Docker Hub](https://hub.docker.com/r/dieters877565/bidi2pdf) by @dieter-medium
|
36
|
+
|
13
37
|
## [0.1.5] - 2025-04-10
|
14
38
|
|
15
39
|
### 📝 Docs
|
@@ -92,3 +116,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
92
116
|
## [0.1.0] - 2025-03-26
|
93
117
|
|
94
118
|
- Initial release
|
119
|
+
|
120
|
+
[unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.6..HEAD
|
121
|
+
|
122
|
+
[0.1.6]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.5..v0.1.6
|
123
|
+
|
124
|
+
[0.1.5]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.4..v0.1.5
|
125
|
+
|
126
|
+
[0.1.4]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.3..v0.1.4
|
data/README.md
CHANGED
@@ -7,8 +7,8 @@
|
|
7
7
|
|
8
8
|
# 📄 Bidi2pdf – Bulletproof PDF generation via Chrome's BiDi Protocol
|
9
9
|
|
10
|
-
**Bidi2pdf** is a powerful Ruby gem that transforms modern web pages into high-fidelity PDFs using Chrome’s
|
11
|
-
|
10
|
+
**Bidi2pdf** is a powerful Ruby gem that transforms modern web pages into high-fidelity PDFs using Chrome’s
|
11
|
+
**BiDirectional (BiDi)** protocol. Whether you're automating reports, archiving websites, or shipping documentation,
|
12
12
|
Bidi2pdf gives you **precision, flexibility, and full control**.
|
13
13
|
|
14
14
|
---
|
@@ -20,7 +20,9 @@ Bidi2pdf gives you **precision, flexibility, and full control**.
|
|
20
20
|
✅ **Smart waiting** – Wait for complete page load or network idle
|
21
21
|
✅ **Headless support** – Run quietly in the background
|
22
22
|
✅ **Docker-ready** – Plug and play with containers
|
23
|
-
✅ **Modern architecture** – Built on Chrome's next-gen BiDi protocol
|
23
|
+
✅ **Modern architecture** – Built on Chrome's next-gen BiDi protocol
|
24
|
+
✅ **Network logging** – Know which requests fail during rendering
|
25
|
+
✅ **Console log capture** – See what goes wrong inside the browser
|
24
26
|
|
25
27
|
---
|
26
28
|
|
@@ -96,8 +98,8 @@ launcher.launch
|
|
96
98
|
require "bidi2pdf"
|
97
99
|
|
98
100
|
Bidi2pdf::DSL.with_tab(headless: true) do |tab|
|
99
|
-
tab.
|
100
|
-
tab.
|
101
|
+
tab.navigate_to("https://example.com")
|
102
|
+
tab.wait_until_network_idle
|
101
103
|
tab.print("example.pdf")
|
102
104
|
end
|
103
105
|
```
|
@@ -142,12 +144,12 @@ tab.basic_auth(url_patterns: [{ type: "pattern", protocol: "https", hostname: "e
|
|
142
144
|
username: "username", password: "secret")
|
143
145
|
|
144
146
|
# 4. Render PDF
|
145
|
-
tab.
|
147
|
+
tab.navigate_to "https://example.com"
|
146
148
|
|
147
149
|
# Alternative: send html code to the browser
|
148
|
-
# tab.
|
150
|
+
# tab.render_html_content("<html>...</html>")
|
149
151
|
|
150
|
-
tab.
|
152
|
+
tab.wait_until_network_idle
|
151
153
|
tab.print("my.pdf")
|
152
154
|
|
153
155
|
# 5. Cleanup
|
@@ -162,15 +164,36 @@ session.close
|
|
162
164
|
|
163
165
|
## 🐳 Docker Support
|
164
166
|
|
165
|
-
### Build & Run
|
167
|
+
### 🛠️ Build & Run Locally
|
166
168
|
|
167
169
|
```bash
|
170
|
+
# Prepare the environment
|
168
171
|
rake build
|
172
|
+
|
173
|
+
# Build the Docker image
|
169
174
|
docker build -t bidi2pdf -f docker/Dockerfile .
|
170
|
-
|
175
|
+
|
176
|
+
# Run the container and generate a PDF
|
177
|
+
docker run -it --rm \
|
178
|
+
-v ./output:/reports \
|
179
|
+
bidi2pdf \
|
180
|
+
bidi2pdf render --url=https://example.com --output /reports/example.pdf
|
181
|
+
|
182
|
+
```
|
183
|
+
|
184
|
+
### ⚡ Use the Prebuilt Image (Recommended for Fast Start)
|
185
|
+
|
186
|
+
Grab it directly from [Docker Hub](https://hub.docker.com/r/dieters877565/bidi2pdf)
|
187
|
+
|
188
|
+
```bash
|
189
|
+
docker run -it --rm \
|
190
|
+
-v ./output:/reports \
|
191
|
+
dieters877565/bidi2pdf:main-slim \
|
171
192
|
bidi2pdf render --url=https://example.com --output /reports/example.pdf
|
172
193
|
```
|
173
194
|
|
195
|
+
✅ Tip: Mount your local directory (e.g. ./output) to /reports in the container to easily access the generated PDFs.
|
196
|
+
|
174
197
|
### Docker Compose
|
175
198
|
|
176
199
|
```bash
|
data/docker/Dockerfile
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
FROM ruby:3.3
|
2
2
|
|
3
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
4
|
+
|
3
5
|
# Install dependencies
|
4
|
-
RUN apt-get update &&
|
5
|
-
apt-get install -y \
|
6
|
-
chromium \
|
6
|
+
RUN apt-get update && apt-get upgrade &&\
|
7
|
+
apt-get install -y --no-install-recommends\
|
8
|
+
chromium chromium-driver\
|
7
9
|
libglib2.0-0 \
|
8
10
|
libnss3 \
|
9
11
|
libxss1 \
|
@@ -19,6 +21,12 @@ RUN apt-get update && \
|
|
19
21
|
# Create a non-root user
|
20
22
|
RUN groupadd -r appuser && useradd -r -g appuser -m -d /home/appuser appuser
|
21
23
|
|
24
|
+
# ARM compatibility workaround:
|
25
|
+
# On ARM architectures (such as Apple Silicon), downloading chromedriver via automated scripts may fail or cause ELF binary errors,
|
26
|
+
# such as "rosetta error: failed to open elf at /lib64/ld-linux-x86-64.so.2".
|
27
|
+
# To avoid these issues, we directly install 'chromium-driver' via the package manager and explicitly create a symlink in the expected location.
|
28
|
+
|
29
|
+
RUN mkdir -p /home/appuser/.webdrivers && ln -s /usr/bin/chromedriver /home/appuser/.webdrivers/chromedriver
|
22
30
|
|
23
31
|
# Set working directory
|
24
32
|
WORKDIR /app
|
@@ -2,9 +2,11 @@ FROM ruby:3.3
|
|
2
2
|
|
3
3
|
ARG CHROMEDRIVER_PORT=3000
|
4
4
|
|
5
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
6
|
+
|
5
7
|
# Install dependencies
|
6
|
-
RUN apt-get update && \
|
7
|
-
apt-get install -y \
|
8
|
+
RUN apt-get update && apt-get upgrade && \
|
9
|
+
apt-get install -y --no-install-recommends\
|
8
10
|
chromium \
|
9
11
|
libglib2.0-0 \
|
10
12
|
libnss3 \
|
@@ -0,0 +1,75 @@
|
|
1
|
+
FROM ruby:3.3-slim AS builder
|
2
|
+
|
3
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
4
|
+
|
5
|
+
# Install dependencies
|
6
|
+
RUN apt-get update && apt-get upgrade && \
|
7
|
+
apt-get install -y --no-install-recommends \
|
8
|
+
chromium \
|
9
|
+
libglib2.0-0 \
|
10
|
+
libnss3 \
|
11
|
+
libxss1 \
|
12
|
+
libasound2 \
|
13
|
+
libatk-bridge2.0-0 \
|
14
|
+
libgtk-3-0 \
|
15
|
+
libdrm2 \
|
16
|
+
curl \
|
17
|
+
unzip \
|
18
|
+
xvfb \
|
19
|
+
build-essential \
|
20
|
+
libpq-dev pkg-config \
|
21
|
+
&& rm -rf /var/lib/apt/lists/*
|
22
|
+
|
23
|
+
# Set working directory
|
24
|
+
WORKDIR /app
|
25
|
+
|
26
|
+
# Copy your gem into container
|
27
|
+
COPY ./pkg/bidi2pdf-*.gem ./
|
28
|
+
|
29
|
+
RUN gem install ./bidi2pdf-*.gem
|
30
|
+
|
31
|
+
|
32
|
+
# Stage 2
|
33
|
+
|
34
|
+
FROM ruby:3.3-slim
|
35
|
+
|
36
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
37
|
+
|
38
|
+
# Install dependencies
|
39
|
+
RUN apt-get update && apt-get upgrade &&\
|
40
|
+
apt-get install -y --no-install-recommends\
|
41
|
+
chromium chromium-driver\
|
42
|
+
libglib2.0-0 \
|
43
|
+
libnss3 \
|
44
|
+
libxss1 \
|
45
|
+
libasound2 \
|
46
|
+
libatk-bridge2.0-0 \
|
47
|
+
libgtk-3-0 \
|
48
|
+
libdrm2 \
|
49
|
+
curl \
|
50
|
+
unzip \
|
51
|
+
xvfb \
|
52
|
+
&& rm -rf /var/lib/apt/lists/*
|
53
|
+
|
54
|
+
COPY --from=builder /usr/local/bundle /usr/local/bundle
|
55
|
+
|
56
|
+
# Create a non-root user
|
57
|
+
RUN groupadd -r appuser && useradd -r -g appuser -m -d /home/appuser appuser
|
58
|
+
|
59
|
+
# ARM compatibility workaround:
|
60
|
+
# On ARM architectures (such as Apple Silicon), downloading chromedriver via automated scripts may fail or cause ELF binary errors,
|
61
|
+
# such as "rosetta error: failed to open elf at /lib64/ld-linux-x86-64.so.2".
|
62
|
+
# To avoid these issues, we directly install 'chromium-driver' via the package manager and explicitly create a symlink in the expected location.
|
63
|
+
|
64
|
+
RUN mkdir -p /home/appuser/.webdrivers && ln -s /usr/bin/chromedriver /home/appuser/.webdrivers/chromedriver
|
65
|
+
|
66
|
+
# Set working directory
|
67
|
+
WORKDIR /app
|
68
|
+
|
69
|
+
RUN chown -R appuser:appuser /app
|
70
|
+
|
71
|
+
# Switch to non-root user
|
72
|
+
USER appuser
|
73
|
+
|
74
|
+
CMD ["/usr/bin/bash"]
|
75
|
+
|
@@ -3,13 +3,14 @@
|
|
3
3
|
require "base64"
|
4
4
|
|
5
5
|
require_relative "network_events"
|
6
|
+
require_relative "logger_events"
|
6
7
|
require_relative "auth_interceptor"
|
7
8
|
require_relative "add_headers_interceptor"
|
8
9
|
|
9
10
|
module Bidi2pdf
|
10
11
|
module Bidi
|
11
12
|
class BrowserTab
|
12
|
-
attr_reader :client, :browsing_context_id, :user_context_id, :tabs, :network_events, :open
|
13
|
+
attr_reader :client, :browsing_context_id, :user_context_id, :tabs, :network_events, :open, :logger_events
|
13
14
|
|
14
15
|
def initialize(client, browsing_context_id, user_context_id)
|
15
16
|
@client = client
|
@@ -17,6 +18,7 @@ module Bidi2pdf
|
|
17
18
|
@user_context_id = user_context_id
|
18
19
|
@tabs = []
|
19
20
|
@network_events = NetworkEvents.new browsing_context_id
|
21
|
+
@logger_events = LoggerEvents.new browsing_context_id
|
20
22
|
@open = true
|
21
23
|
end
|
22
24
|
|
@@ -77,10 +79,13 @@ module Bidi2pdf
|
|
77
79
|
).tap { |interceptor| interceptor.register_with_client(client: client) }
|
78
80
|
end
|
79
81
|
|
80
|
-
def
|
81
|
-
client.on_event("network.responseStarted", "network.responseCompleted", "network.fetchError",
|
82
|
+
def navigate_to(url)
|
83
|
+
client.on_event("network.beforeRequestSent", "network.responseStarted", "network.responseCompleted", "network.fetchError",
|
82
84
|
&network_events.method(:handle_event))
|
83
85
|
|
86
|
+
client.on_event("log.entryAdded",
|
87
|
+
&logger_events.method(:handle_event))
|
88
|
+
|
84
89
|
cmd = Bidi2pdf::Bidi::Commands::BrowsingContextNavigate.new url: url, context: browsing_context_id
|
85
90
|
|
86
91
|
client.send_cmd_and_wait(cmd) do |response|
|
@@ -88,11 +93,11 @@ module Bidi2pdf
|
|
88
93
|
end
|
89
94
|
end
|
90
95
|
|
91
|
-
def
|
96
|
+
def render_html_content(html_content)
|
92
97
|
base64_encoded = Base64.strict_encode64(html_content)
|
93
98
|
data_url = "data:text/html;charset=utf-8;base64,#{base64_encoded}"
|
94
99
|
|
95
|
-
|
100
|
+
navigate_to(data_url)
|
96
101
|
end
|
97
102
|
|
98
103
|
def execute_script(script)
|
@@ -104,8 +109,29 @@ module Bidi2pdf
|
|
104
109
|
end
|
105
110
|
end
|
106
111
|
|
107
|
-
def
|
108
|
-
network_events.
|
112
|
+
def wait_until_network_idle(timeout: 10, poll_interval: 0.1)
|
113
|
+
network_events.wait_until_network_idle(timeout: timeout, poll_interval: poll_interval)
|
114
|
+
end
|
115
|
+
|
116
|
+
def log_network_traffic(format: :console, output: nil, print_options: { background: true }, &block)
|
117
|
+
format = format.to_sym
|
118
|
+
|
119
|
+
if format == :console
|
120
|
+
network_events.log_network_traffic format: :console
|
121
|
+
elsif format == :pdf
|
122
|
+
html_content = network_events.log_network_traffic format: :html
|
123
|
+
|
124
|
+
return unless html_content
|
125
|
+
|
126
|
+
logging_tab = create_browser_tab
|
127
|
+
|
128
|
+
logging_tab.render_html_content(html_content)
|
129
|
+
logging_tab.wait_until_network_idle
|
130
|
+
|
131
|
+
logging_tab.print(output, print_options: print_options, &block)
|
132
|
+
|
133
|
+
logging_tab.close
|
134
|
+
end
|
109
135
|
end
|
110
136
|
|
111
137
|
def close
|
@@ -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
@@ -51,6 +51,12 @@ module Bidi2pdf
|
|
51
51
|
option :log_level,
|
52
52
|
type: :string,
|
53
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"
|
54
60
|
|
55
61
|
option :background, type: :boolean, default: true, desc: "Print background graphics"
|
56
62
|
option :margin_top, type: :numeric, default: 1.0, desc: "Top margin in inches"
|
@@ -64,6 +70,12 @@ module Bidi2pdf
|
|
64
70
|
option :scale, type: :numeric, default: 1.0, desc: "Scale between 0.1 and 2.0"
|
65
71
|
option :shrink_to_fit, type: :boolean, default: true, desc: "Shrink content to fit page"
|
66
72
|
|
73
|
+
class << self
|
74
|
+
def exit_on_failure?
|
75
|
+
true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
67
79
|
def render
|
68
80
|
load_config
|
69
81
|
|
@@ -198,7 +210,8 @@ module Bidi2pdf
|
|
198
210
|
headless: merged_options[:headless],
|
199
211
|
wait_window_loaded: merged_options[:wait_window_loaded],
|
200
212
|
wait_network_idle: merged_options[:wait_network_idle],
|
201
|
-
print_options: print_options
|
213
|
+
print_options: print_options,
|
214
|
+
network_log_format: merged_options[:network_log_format]
|
202
215
|
)
|
203
216
|
end
|
204
217
|
end
|
@@ -208,6 +221,9 @@ module Bidi2pdf
|
|
208
221
|
def configure
|
209
222
|
Bidi2pdf.configure do |config|
|
210
223
|
config.logger.level = log_level
|
224
|
+
|
225
|
+
config.network_events_logger.level = Logger::INFO if merged_options[:log_network_traffic]
|
226
|
+
|
211
227
|
config.default_timeout = merged_options[:default_timeout]
|
212
228
|
|
213
229
|
Chromedriver::Binary.configure do |c|
|
data/lib/bidi2pdf/launcher.rb
CHANGED
@@ -8,7 +8,7 @@ module Bidi2pdf
|
|
8
8
|
class Launcher
|
9
9
|
# rubocop:disable Metrics/ParameterLists
|
10
10
|
def initialize(url:, inputfile:, output:, cookies:, headers:, auth:, headless: true, port: 0, wait_window_loaded: false,
|
11
|
-
wait_network_idle: false, print_options: {}, remote_browser_url: nil)
|
11
|
+
wait_network_idle: false, print_options: {}, remote_browser_url: nil, network_log_format: :console)
|
12
12
|
@url = url
|
13
13
|
@inputfile = inputfile
|
14
14
|
@port = port
|
@@ -23,6 +23,7 @@ module Bidi2pdf
|
|
23
23
|
@print_options = print_options || {}
|
24
24
|
@remote_browser_url = remote_browser_url
|
25
25
|
@custom_session = nil
|
26
|
+
@network_log_format = network_log_format
|
26
27
|
end
|
27
28
|
|
28
29
|
# rubocop:enable Metrics/ParameterLists
|
@@ -38,7 +39,8 @@ module Bidi2pdf
|
|
38
39
|
auth: @auth,
|
39
40
|
wait_window_loaded: @wait_window_loaded,
|
40
41
|
wait_network_idle: @wait_network_idle,
|
41
|
-
print_options: @print_options
|
42
|
+
print_options: @print_options,
|
43
|
+
network_log_format: @network_log_format
|
42
44
|
)
|
43
45
|
runner.run
|
44
46
|
end
|
@@ -2,8 +2,9 @@
|
|
2
2
|
|
3
3
|
module Bidi2pdf
|
4
4
|
class SessionRunner
|
5
|
+
# rubocop: disable Metrics/ParameterLists
|
5
6
|
def initialize(session:, url:, inputfile:, output:, cookies: {}, headers: {}, auth: {}, wait_window_loaded: false,
|
6
|
-
wait_network_idle: false, print_options: {})
|
7
|
+
wait_network_idle: false, print_options: {}, network_log_format: :console)
|
7
8
|
@session = session
|
8
9
|
@url = url
|
9
10
|
@inputfile = inputfile
|
@@ -14,8 +15,11 @@ module Bidi2pdf
|
|
14
15
|
@wait_window_loaded = wait_window_loaded
|
15
16
|
@wait_network_idle = wait_network_idle
|
16
17
|
@print_options = print_options || {}
|
18
|
+
@network_log_format = network_log_format
|
17
19
|
end
|
18
20
|
|
21
|
+
# rubocop: enable Metrics/ParameterLists
|
22
|
+
|
19
23
|
def run
|
20
24
|
@session.start
|
21
25
|
@session.client.on_close { Bidi2pdf.logger.info "WebSocket closed" }
|
@@ -72,23 +76,27 @@ module Bidi2pdf
|
|
72
76
|
)
|
73
77
|
end
|
74
78
|
|
79
|
+
# rubocop: disable Metrics/AbcSize
|
75
80
|
def run_flow
|
76
81
|
@session.status
|
77
82
|
@session.user_contexts
|
78
83
|
|
79
84
|
if @url
|
80
|
-
@tab.
|
85
|
+
@tab.navigate_to(@url)
|
81
86
|
else
|
82
87
|
Bidi2pdf.logger.info "Loading HTML file #{@inputfile}"
|
83
88
|
data = File.read(@inputfile)
|
84
|
-
@tab.
|
89
|
+
@tab.render_html_content(data)
|
85
90
|
end
|
86
91
|
|
87
92
|
if @wait_network_idle
|
88
93
|
Bidi2pdf.logger.info "Waiting for network idle"
|
89
|
-
@tab.
|
94
|
+
@tab.wait_until_network_idle
|
90
95
|
end
|
91
96
|
|
97
|
+
log_output_file = (@output || "report").sub(/\.pdf$/, "-network.pdf") # only need, when html output
|
98
|
+
@tab.log_network_traffic format: @network_log_format, output: log_output_file
|
99
|
+
|
92
100
|
if @wait_window_loaded
|
93
101
|
Bidi2pdf.logger.info "Waiting for window to be loaded"
|
94
102
|
@tab.execute_script <<-EOF_SCRIPT
|
@@ -102,6 +110,8 @@ module Bidi2pdf
|
|
102
110
|
@window.close
|
103
111
|
end
|
104
112
|
|
113
|
+
# rubocop: enable Metrics/AbcSize
|
114
|
+
|
105
115
|
def uri
|
106
116
|
@uri ||= URI(@url)
|
107
117
|
end
|
data/lib/bidi2pdf/version.rb
CHANGED
data/lib/bidi2pdf.rb
CHANGED
@@ -26,12 +26,14 @@ module Bidi2pdf
|
|
26
26
|
class PrintError < Error; end
|
27
27
|
|
28
28
|
@logger = Logger.new($stdout)
|
29
|
+
@network_events_logger = Logger.new($stdout)
|
29
30
|
@logger.level = Logger::INFO
|
31
|
+
@network_events_logger.level = Logger::FATAL
|
30
32
|
|
31
33
|
@default_timeout = 60
|
32
34
|
|
33
35
|
class << self
|
34
|
-
attr_accessor :logger, :default_timeout
|
36
|
+
attr_accessor :logger, :default_timeout, :network_events_logger
|
35
37
|
|
36
38
|
# Allow configuration through a block
|
37
39
|
def configure
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bidi2pdf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dieter S.
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-04-
|
11
|
+
date: 2025-04-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: base64
|
@@ -310,6 +310,7 @@ files:
|
|
310
310
|
- cliff.toml
|
311
311
|
- docker/Dockerfile
|
312
312
|
- docker/Dockerfile.chromedriver
|
313
|
+
- docker/Dockerfile.slim
|
313
314
|
- docker/docker-compose.yml
|
314
315
|
- docker/entrypoint.sh
|
315
316
|
- docker/nginx/default.conf
|
@@ -346,7 +347,12 @@ files:
|
|
346
347
|
- lib/bidi2pdf/bidi/connection_manager.rb
|
347
348
|
- lib/bidi2pdf/bidi/event_manager.rb
|
348
349
|
- lib/bidi2pdf/bidi/interceptor.rb
|
350
|
+
- lib/bidi2pdf/bidi/logger_events.rb
|
349
351
|
- lib/bidi2pdf/bidi/network_event.rb
|
352
|
+
- lib/bidi2pdf/bidi/network_event_formatters.rb
|
353
|
+
- lib/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rb
|
354
|
+
- lib/bidi2pdf/bidi/network_event_formatters/network_event_formatter_utils.rb
|
355
|
+
- lib/bidi2pdf/bidi/network_event_formatters/network_event_html_formatter.rb
|
350
356
|
- lib/bidi2pdf/bidi/network_events.rb
|
351
357
|
- lib/bidi2pdf/bidi/session.rb
|
352
358
|
- lib/bidi2pdf/bidi/user_context.rb
|