perchfall 0.3.2 → 0.4.0
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 +17 -1
- data/lib/perchfall/client.rb +38 -18
- data/lib/perchfall/parsers/playwright_json_parser.rb +22 -4
- data/lib/perchfall/playwright_invoker.rb +6 -5
- data/lib/perchfall/report.rb +9 -3
- data/lib/perchfall/resource.rb +15 -0
- data/lib/perchfall/version.rb +1 -1
- data/lib/perchfall.rb +1 -0
- data/playwright/check.js +39 -10
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d608f361607d90f51b79aee9aab5c4585dfbc3c5f5a7556c60eef9154588664
|
|
4
|
+
data.tar.gz: 17a9717ef25c6c5e81a38fa8a255730013320d38aa5636f15b60d4ed15252297
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: febd8246e1b5560784ab0530a50191d274697d863e50b73409e18acadd762fff8669a67b8b35894bf3f31ec386509c6f61e63b9ce1c9b74fda59fe3f0cfdb19b
|
|
7
|
+
data.tar.gz: cc4a837fe3f82be4686854348d556e60b362d7336c0f5f2277d8e3c4edad42f994234484c64b588b12d7ff6da90ce48448200edd73b33865d35a7f3a07ab35b9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-03-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `capture_resources: false` option on `Perchfall.run` / `Client#run` — when `true`, collects metadata for every resource loaded during the page run and stores large ones on the report
|
|
15
|
+
- `report.resources` — array of `Resource` objects (url, http_method, status, content_type, transfer_size, resource_type) whose transfer size met or exceeded the configured threshold, plus any resource whose size could not be determined (absent `content-length`)
|
|
16
|
+
- `large_resource_threshold_bytes:` option (default 200 000 bytes / 200 KB) — controls the minimum transfer size for a resource to appear in `report.resources`; only meaningful when `capture_resources: true`
|
|
17
|
+
- `Perchfall::Resource` value object (`Data.define`) with all resource fields; `transfer_size` is `Integer` or `nil` (nil means unknown, not zero)
|
|
18
|
+
|
|
19
|
+
### Notes
|
|
20
|
+
|
|
21
|
+
- Resource capture is opt-in and off by default. No overhead is incurred unless `capture_resources: true` is passed.
|
|
22
|
+
- Resources with an unknown transfer size (`nil`) are always included in `report.resources` — they cannot be proven to be below the threshold.
|
|
23
|
+
- `report.ok?` is not affected by `report.resources` — size is a metric, not a pass/fail signal.
|
|
24
|
+
|
|
10
25
|
## [0.3.2] - 2026-03-19
|
|
11
26
|
|
|
12
27
|
### Added
|
|
@@ -74,7 +89,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
74
89
|
- Full dependency injection throughout — test suite runs in ~0.4 s with no browser, Node, or network required
|
|
75
90
|
- GitHub Actions CI workflow (unit suite) and manual Playwright smoke check workflow
|
|
76
91
|
|
|
77
|
-
[Unreleased]: https://github.com/beflagrant/perchfall/compare/v0.
|
|
92
|
+
[Unreleased]: https://github.com/beflagrant/perchfall/compare/v0.4.0...HEAD
|
|
93
|
+
[0.4.0]: https://github.com/beflagrant/perchfall/compare/v0.3.2...v0.4.0
|
|
78
94
|
[0.3.2]: https://github.com/beflagrant/perchfall/compare/v0.3.1...v0.3.2
|
|
79
95
|
[0.3.1]: https://github.com/beflagrant/perchfall/compare/v0.2.0...v0.3.1
|
|
80
96
|
[0.2.0]: https://github.com/beflagrant/perchfall/compare/v0.1.0...v0.2.0
|
data/lib/perchfall/client.rb
CHANGED
|
@@ -82,29 +82,49 @@ module Perchfall
|
|
|
82
82
|
report
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
DEFAULT_RESOURCE_THRESHOLD = Parsers::PlaywrightJsonParser::DEFAULT_LARGE_RESOURCE_THRESHOLD_BYTES
|
|
86
|
+
|
|
87
|
+
RunOptions = Data.define(
|
|
88
|
+
:url, :ignore, :wait_until, :timeout_ms, :scenario_name,
|
|
89
|
+
:timestamp, :cache_profile, :capture_resources, :large_resource_threshold_bytes
|
|
90
|
+
) do
|
|
91
|
+
def self.from_kwargs(url:, ignore: [], wait_until: 'load', timeout_ms: 30_000,
|
|
92
|
+
scenario_name: nil, timestamp: Time.now.utc,
|
|
93
|
+
cache_profile: :query_bust, capture_resources: false,
|
|
94
|
+
large_resource_threshold_bytes: DEFAULT_RESOURCE_THRESHOLD)
|
|
95
|
+
new(url: url, ignore: ignore, wait_until: wait_until, timeout_ms: timeout_ms,
|
|
96
|
+
scenario_name: scenario_name, timestamp: timestamp, cache_profile: cache_profile,
|
|
97
|
+
capture_resources: capture_resources,
|
|
98
|
+
large_resource_threshold_bytes: large_resource_threshold_bytes)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
85
102
|
private
|
|
86
103
|
|
|
87
|
-
def invoke(url:,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
def invoke(url:, **kwargs)
|
|
105
|
+
opts = RunOptions.from_kwargs(url: url, **kwargs)
|
|
106
|
+
profile = resolve_cache_profile!(opts.cache_profile)
|
|
107
|
+
validate_wait_until!(opts.wait_until)
|
|
108
|
+
validate_timeout_ms!(opts.timeout_ms)
|
|
109
|
+
effective_url = profile[:bust_url] ? append_cache_buster(opts.url) : opts.url
|
|
92
110
|
@validator.validate!(effective_url)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
timestamp:
|
|
102
|
-
cache_profile: cache_profile
|
|
111
|
+
@limiter.acquire { @invoker.run(**build_invoker_opts(opts, effective_url, profile)) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_invoker_opts(opts, effective_url, profile)
|
|
115
|
+
result = {
|
|
116
|
+
url: effective_url, original_url: opts.url,
|
|
117
|
+
ignore: Perchfall::DEFAULT_IGNORE_RULES + opts.ignore,
|
|
118
|
+
wait_until: opts.wait_until, timeout_ms: opts.timeout_ms,
|
|
119
|
+
scenario_name: opts.scenario_name, timestamp: opts.timestamp,
|
|
120
|
+
cache_profile: opts.cache_profile
|
|
103
121
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
result[:extra_headers] = profile[:headers] unless profile[:headers].empty?
|
|
123
|
+
if opts.capture_resources
|
|
124
|
+
result[:capture_resources] = true
|
|
125
|
+
result[:large_resource_threshold_bytes] = opts.large_resource_threshold_bytes
|
|
107
126
|
end
|
|
127
|
+
result
|
|
108
128
|
end
|
|
109
129
|
|
|
110
130
|
private
|
|
@@ -13,16 +13,18 @@ module Perchfall
|
|
|
13
13
|
@filter = filter
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
DEFAULT_LARGE_RESOURCE_THRESHOLD_BYTES = 200_000
|
|
17
|
+
|
|
18
|
+
def parse(raw_json, timestamp:, scenario_name: nil, original_url: nil, cache_profile: nil, capture_resources: false, large_resource_threshold_bytes: DEFAULT_LARGE_RESOURCE_THRESHOLD_BYTES)
|
|
17
19
|
data = JSON.parse(raw_json, symbolize_names: true)
|
|
18
|
-
build_report(data, scenario_name: scenario_name, timestamp: timestamp, original_url: original_url, cache_profile: cache_profile)
|
|
20
|
+
build_report(data, scenario_name: scenario_name, timestamp: timestamp, original_url: original_url, cache_profile: cache_profile, capture_resources: capture_resources, large_resource_threshold_bytes: large_resource_threshold_bytes)
|
|
19
21
|
rescue JSON::ParserError => e
|
|
20
22
|
raise Errors::ParseError, "Invalid JSON from Playwright script: #{e.message}"
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
private
|
|
24
26
|
|
|
25
|
-
def build_report(data, scenario_name:, timestamp:, original_url: nil, cache_profile: nil)
|
|
27
|
+
def build_report(data, scenario_name:, timestamp:, original_url: nil, cache_profile: nil, capture_resources: false, large_resource_threshold_bytes: DEFAULT_LARGE_RESOURCE_THRESHOLD_BYTES)
|
|
26
28
|
net_filtered = @filter.filter_network(parse_network_errors(data.fetch(:network_errors, [])))
|
|
27
29
|
console_filtered = @filter.filter_console(parse_console_errors(data.fetch(:console_errors, [])))
|
|
28
30
|
|
|
@@ -38,7 +40,8 @@ module Perchfall
|
|
|
38
40
|
error: data[:error],
|
|
39
41
|
scenario_name: scenario_name,
|
|
40
42
|
timestamp: timestamp,
|
|
41
|
-
cache_profile: cache_profile
|
|
43
|
+
cache_profile: cache_profile,
|
|
44
|
+
resources: capture_resources ? parse_resources(data.fetch(:resources, []), threshold_bytes: large_resource_threshold_bytes) : []
|
|
42
45
|
)
|
|
43
46
|
rescue KeyError => e
|
|
44
47
|
raise Errors::ParseError, "Playwright JSON missing required field: #{e.message}"
|
|
@@ -67,6 +70,21 @@ module Perchfall
|
|
|
67
70
|
rescue KeyError => e
|
|
68
71
|
raise Errors::ParseError, "Malformed console_error entry: #{e.message}"
|
|
69
72
|
end
|
|
73
|
+
|
|
74
|
+
def parse_resources(raw, threshold_bytes:)
|
|
75
|
+
raw.filter_map do |item|
|
|
76
|
+
size = item[:transfer_size]
|
|
77
|
+
next if size && size < threshold_bytes
|
|
78
|
+
Resource.new(
|
|
79
|
+
url: item[:url],
|
|
80
|
+
http_method: item[:method],
|
|
81
|
+
status: item[:status],
|
|
82
|
+
content_type: item[:content_type],
|
|
83
|
+
transfer_size: size,
|
|
84
|
+
resource_type: item[:resource_type]
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
70
88
|
end
|
|
71
89
|
end
|
|
72
90
|
end
|
|
@@ -24,10 +24,10 @@ module Perchfall
|
|
|
24
24
|
@script_path = script_path
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def run(url:, timestamp:, timeout_ms: 30_000, wait_until: "load", scenario_name: nil, ignore: [], original_url: nil, extra_headers: {}, cache_profile: nil)
|
|
27
|
+
def run(url:, timestamp:, timeout_ms: 30_000, wait_until: "load", scenario_name: nil, ignore: [], original_url: nil, extra_headers: {}, cache_profile: nil, capture_resources: false, large_resource_threshold_bytes: Parsers::PlaywrightJsonParser::DEFAULT_LARGE_RESOURCE_THRESHOLD_BYTES)
|
|
28
28
|
parser = build_parser(ignore)
|
|
29
|
-
result = execute(build_command(url: url, timeout_ms: timeout_ms, wait_until: wait_until, extra_headers: extra_headers))
|
|
30
|
-
parse(result, parser: parser, scenario_name: scenario_name, timestamp: timestamp, original_url: original_url || url, cache_profile: cache_profile)
|
|
29
|
+
result = execute(build_command(url: url, timeout_ms: timeout_ms, wait_until: wait_until, extra_headers: extra_headers, capture_resources: capture_resources))
|
|
30
|
+
parse(result, parser: parser, scenario_name: scenario_name, timestamp: timestamp, original_url: original_url || url, cache_profile: cache_profile, capture_resources: capture_resources, large_resource_threshold_bytes: large_resource_threshold_bytes)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
private
|
|
@@ -36,9 +36,10 @@ module Perchfall
|
|
|
36
36
|
Parsers::PlaywrightJsonParser.new(filter: ErrorFilter.new(rules: ignore_rules))
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def build_command(url:, timeout_ms:, wait_until:, extra_headers: {})
|
|
39
|
+
def build_command(url:, timeout_ms:, wait_until:, extra_headers: {}, capture_resources: false)
|
|
40
40
|
cmd = ["node", @script_path, "--url", url, "--timeout", timeout_ms.to_s, "--wait-until", wait_until]
|
|
41
41
|
cmd += ["--headers", extra_headers.to_json] unless extra_headers.empty?
|
|
42
|
+
cmd += ["--capture-resources"] if capture_resources
|
|
42
43
|
cmd
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -57,7 +58,7 @@ module Perchfall
|
|
|
57
58
|
)
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
parser.parse(result.stdout,
|
|
61
|
+
parser.parse(result.stdout, timestamp: opts[:timestamp], scenario_name: opts[:scenario_name], original_url: opts[:original_url], cache_profile: opts[:cache_profile], capture_resources: opts[:capture_resources] || false, large_resource_threshold_bytes: opts[:large_resource_threshold_bytes] || Parsers::PlaywrightJsonParser::DEFAULT_LARGE_RESOURCE_THRESHOLD_BYTES)
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
end
|
data/lib/perchfall/report.rb
CHANGED
|
@@ -18,10 +18,13 @@ module Perchfall
|
|
|
18
18
|
# ignored_console_errors - Array<ConsoleError>: errors suppressed by ignore rules
|
|
19
19
|
# error - String or nil: set only when status == "error"
|
|
20
20
|
# cache_profile - Symbol or nil: the cache profile used for this run
|
|
21
|
+
# resources - Array<Resource>: resources that exceeded the configured size
|
|
22
|
+
# threshold; only populated when capture_resources: true was passed
|
|
21
23
|
class Report
|
|
22
24
|
attr_reader :status, :url, :scenario_name, :timestamp, :duration_ms,
|
|
23
25
|
:http_status, :network_errors, :ignored_network_errors,
|
|
24
|
-
:console_errors, :ignored_console_errors, :error, :cache_profile
|
|
26
|
+
:console_errors, :ignored_console_errors, :error, :cache_profile,
|
|
27
|
+
:resources
|
|
25
28
|
|
|
26
29
|
def initialize(
|
|
27
30
|
status:,
|
|
@@ -35,7 +38,8 @@ module Perchfall
|
|
|
35
38
|
ignored_console_errors: [],
|
|
36
39
|
scenario_name: nil,
|
|
37
40
|
timestamp: Time.now.utc,
|
|
38
|
-
cache_profile: nil
|
|
41
|
+
cache_profile: nil,
|
|
42
|
+
resources: []
|
|
39
43
|
)
|
|
40
44
|
@status = status.freeze
|
|
41
45
|
@url = url.freeze
|
|
@@ -49,6 +53,7 @@ module Perchfall
|
|
|
49
53
|
@ignored_console_errors = ignored_console_errors.freeze
|
|
50
54
|
@error = error&.freeze
|
|
51
55
|
@cache_profile = cache_profile
|
|
56
|
+
@resources = resources.freeze
|
|
52
57
|
freeze
|
|
53
58
|
end
|
|
54
59
|
|
|
@@ -70,7 +75,8 @@ module Perchfall
|
|
|
70
75
|
console_errors: console_errors.map(&:to_h),
|
|
71
76
|
ignored_console_errors: ignored_console_errors.map(&:to_h),
|
|
72
77
|
error: error,
|
|
73
|
-
cache_profile: cache_profile
|
|
78
|
+
cache_profile: cache_profile,
|
|
79
|
+
resources: resources.map(&:to_h)
|
|
74
80
|
}
|
|
75
81
|
end
|
|
76
82
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Perchfall
|
|
4
|
+
# Represents a single resource loaded during a page run.
|
|
5
|
+
#
|
|
6
|
+
# Attributes:
|
|
7
|
+
# url - String: the resource URL
|
|
8
|
+
# http_method - String: HTTP method (e.g. "GET")
|
|
9
|
+
# status - Integer: HTTP response status code
|
|
10
|
+
# content_type - String or nil: Content-Type header value
|
|
11
|
+
# transfer_size - Integer or nil: wire bytes from Content-Length header;
|
|
12
|
+
# nil means the header was absent (chunked/inline) — unknown, not zero
|
|
13
|
+
# resource_type - String: Playwright resource type (e.g. "image", "script", "stylesheet")
|
|
14
|
+
Resource = Data.define(:url, :http_method, :status, :content_type, :transfer_size, :resource_type)
|
|
15
|
+
end
|
data/lib/perchfall/version.rb
CHANGED
data/lib/perchfall.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "perchfall/error_filter"
|
|
|
10
10
|
require_relative "perchfall/command_runner"
|
|
11
11
|
require_relative "perchfall/concurrency_limiter"
|
|
12
12
|
require_relative "perchfall/url_validator"
|
|
13
|
+
require_relative "perchfall/resource"
|
|
13
14
|
require_relative "perchfall/parsers/playwright_json_parser"
|
|
14
15
|
require_relative "perchfall/playwright_invoker"
|
|
15
16
|
require_relative "perchfall/client"
|
data/playwright/check.js
CHANGED
|
@@ -20,10 +20,11 @@ const { parseArgs } = require("node:util");
|
|
|
20
20
|
|
|
21
21
|
const { values: args } = parseArgs({
|
|
22
22
|
options: {
|
|
23
|
-
url:
|
|
24
|
-
timeout:
|
|
25
|
-
"wait-until":
|
|
26
|
-
headers:
|
|
23
|
+
url: { type: "string" },
|
|
24
|
+
timeout: { type: "string", default: "30000" },
|
|
25
|
+
"wait-until": { type: "string", default: "load" },
|
|
26
|
+
headers: { type: "string", default: "{}" },
|
|
27
|
+
"capture-resources": { type: "boolean", default: false },
|
|
27
28
|
},
|
|
28
29
|
strict: true,
|
|
29
30
|
});
|
|
@@ -33,9 +34,10 @@ if (!args.url) {
|
|
|
33
34
|
process.exit(1);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const TARGET_URL
|
|
37
|
-
const TIMEOUT_MS
|
|
38
|
-
const WAIT_UNTIL
|
|
37
|
+
const TARGET_URL = args.url;
|
|
38
|
+
const TIMEOUT_MS = parseInt(args.timeout, 10);
|
|
39
|
+
const WAIT_UNTIL = args["wait-until"];
|
|
40
|
+
const CAPTURE_RESOURCES = args["capture-resources"];
|
|
39
41
|
|
|
40
42
|
let EXTRA_HEADERS;
|
|
41
43
|
try {
|
|
@@ -66,8 +68,8 @@ try {
|
|
|
66
68
|
// Helpers
|
|
67
69
|
// ---------------------------------------------------------------------------
|
|
68
70
|
|
|
69
|
-
function buildResult({ status, durationMs, httpStatus, networkErrors, consoleErrors, error }) {
|
|
70
|
-
|
|
71
|
+
function buildResult({ status, durationMs, httpStatus, networkErrors, consoleErrors, resources, error }) {
|
|
72
|
+
const result = {
|
|
71
73
|
status,
|
|
72
74
|
url: TARGET_URL,
|
|
73
75
|
duration_ms: durationMs,
|
|
@@ -75,7 +77,25 @@ function buildResult({ status, durationMs, httpStatus, networkErrors, consoleErr
|
|
|
75
77
|
network_errors: networkErrors,
|
|
76
78
|
console_errors: consoleErrors,
|
|
77
79
|
error: error ?? null,
|
|
78
|
-
}
|
|
80
|
+
};
|
|
81
|
+
if (resources !== undefined) result.resources = resources;
|
|
82
|
+
return JSON.stringify(result);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function resolveResources(pending) {
|
|
86
|
+
if (pending === null) return undefined;
|
|
87
|
+
return Promise.all(pending.map(async (res) => {
|
|
88
|
+
const headers = await res.allHeaders();
|
|
89
|
+
const rawLength = headers["content-length"];
|
|
90
|
+
return {
|
|
91
|
+
url: res.url(),
|
|
92
|
+
method: res.request().method(),
|
|
93
|
+
status: res.status(),
|
|
94
|
+
content_type: headers["content-type"] ?? null,
|
|
95
|
+
transfer_size: rawLength != null ? parseInt(rawLength, 10) : null,
|
|
96
|
+
resource_type: res.request().resourceType(),
|
|
97
|
+
};
|
|
98
|
+
}));
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
// ---------------------------------------------------------------------------
|
|
@@ -86,6 +106,7 @@ async function run() {
|
|
|
86
106
|
const startedAt = Date.now();
|
|
87
107
|
const networkErrors = [];
|
|
88
108
|
const consoleErrors = [];
|
|
109
|
+
const pendingResources = CAPTURE_RESOURCES ? [] : null;
|
|
89
110
|
let browser;
|
|
90
111
|
|
|
91
112
|
try {
|
|
@@ -106,6 +127,7 @@ async function run() {
|
|
|
106
127
|
});
|
|
107
128
|
|
|
108
129
|
// Collect non-2xx/3xx responses as network errors too.
|
|
130
|
+
// When resource capture is enabled, stash every response for later processing.
|
|
109
131
|
page.on("response", (response) => {
|
|
110
132
|
const status = response.status();
|
|
111
133
|
if (status >= 400) {
|
|
@@ -115,6 +137,9 @@ async function run() {
|
|
|
115
137
|
failure: `HTTP ${status}`,
|
|
116
138
|
});
|
|
117
139
|
}
|
|
140
|
+
if (pendingResources !== null) {
|
|
141
|
+
pendingResources.push(response);
|
|
142
|
+
}
|
|
118
143
|
});
|
|
119
144
|
|
|
120
145
|
// Collect browser console errors.
|
|
@@ -135,6 +160,7 @@ async function run() {
|
|
|
135
160
|
});
|
|
136
161
|
|
|
137
162
|
const durationMs = Date.now() - startedAt;
|
|
163
|
+
const resources = await resolveResources(pendingResources);
|
|
138
164
|
|
|
139
165
|
process.stdout.write(buildResult({
|
|
140
166
|
status: "ok",
|
|
@@ -142,6 +168,7 @@ async function run() {
|
|
|
142
168
|
httpStatus: response ? response.status() : null,
|
|
143
169
|
networkErrors,
|
|
144
170
|
consoleErrors,
|
|
171
|
+
resources,
|
|
145
172
|
error: null,
|
|
146
173
|
}));
|
|
147
174
|
|
|
@@ -150,6 +177,7 @@ async function run() {
|
|
|
150
177
|
} catch (err) {
|
|
151
178
|
// Page-level failure (timeout, DNS, etc.) — exit 0 so Ruby reads the JSON.
|
|
152
179
|
const durationMs = Date.now() - startedAt;
|
|
180
|
+
const resources = await resolveResources(pendingResources);
|
|
153
181
|
|
|
154
182
|
process.stdout.write(buildResult({
|
|
155
183
|
status: "error",
|
|
@@ -157,6 +185,7 @@ async function run() {
|
|
|
157
185
|
httpStatus: null,
|
|
158
186
|
networkErrors,
|
|
159
187
|
consoleErrors,
|
|
188
|
+
resources,
|
|
160
189
|
error: err.message,
|
|
161
190
|
}));
|
|
162
191
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: perchfall
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jim Remsik
|
|
@@ -89,6 +89,7 @@ files:
|
|
|
89
89
|
- lib/perchfall/parsers/playwright_json_parser.rb
|
|
90
90
|
- lib/perchfall/playwright_invoker.rb
|
|
91
91
|
- lib/perchfall/report.rb
|
|
92
|
+
- lib/perchfall/resource.rb
|
|
92
93
|
- lib/perchfall/url_validator.rb
|
|
93
94
|
- lib/perchfall/version.rb
|
|
94
95
|
- perchfall.gemspec
|