crawlscope 0.7.2 → 0.8.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 +18 -0
- data/README.md +74 -8
- data/lib/crawlscope/browser.rb +18 -1
- data/lib/crawlscope/configuration.rb +6 -1
- data/lib/crawlscope/crawl.rb +5 -2
- data/lib/crawlscope/http.rb +13 -2
- data/lib/crawlscope/page.rb +8 -0
- data/lib/crawlscope/reporter.rb +9 -4
- data/lib/crawlscope/request_headers.rb +34 -0
- data/lib/crawlscope/result.rb +4 -0
- data/lib/crawlscope/run.rb +0 -10
- data/lib/crawlscope/server_timing/reporter.rb +112 -0
- data/lib/crawlscope/server_timing/summary.rb +107 -0
- data/lib/crawlscope/server_timing.rb +138 -0
- data/lib/crawlscope/sitemap.rb +17 -5
- data/lib/crawlscope/version.rb +1 -1
- data/lib/generators/crawlscope/templates/initializer.rb.tt +1 -1
- data/test/crawlscope/browser_test.rb +56 -3
- data/test/crawlscope/configuration_test.rb +50 -10
- data/test/crawlscope/crawl_test.rb +29 -0
- data/test/crawlscope/http_test.rb +60 -1
- data/test/crawlscope/install_generator_test.rb +9 -11
- data/test/crawlscope/reporter_test.rb +64 -6
- data/test/crawlscope/run_test.rb +3 -11
- data/test/crawlscope/server_timing_summary_test.rb +87 -0
- data/test/crawlscope/server_timing_test.rb +101 -0
- data/test/crawlscope/sitemap_test.rb +21 -0
- metadata +7 -1
|
@@ -10,7 +10,7 @@ class CrawlscopeReporterTest < Minitest::Test
|
|
|
10
10
|
base_url: "https://example.com",
|
|
11
11
|
sitemap_path: "/tmp/sitemap.xml",
|
|
12
12
|
urls: ["https://example.com"],
|
|
13
|
-
pages: [
|
|
13
|
+
pages: [page],
|
|
14
14
|
issues: Crawlscope::IssueCollection.new
|
|
15
15
|
)
|
|
16
16
|
|
|
@@ -21,6 +21,7 @@ class CrawlscopeReporterTest < Minitest::Test
|
|
|
21
21
|
assert_includes output, "Crawlscope validation"
|
|
22
22
|
assert_includes output, "Status: OK"
|
|
23
23
|
refute_includes output, "Status: FAILED"
|
|
24
|
+
refute_includes output, "Server Timing:"
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def test_reports_warning_result_with_grouped_one_line_issues
|
|
@@ -46,7 +47,7 @@ class CrawlscopeReporterTest < Minitest::Test
|
|
|
46
47
|
base_url: "https://example.com",
|
|
47
48
|
sitemap_path: "/tmp/sitemap.xml",
|
|
48
49
|
urls: ["https://example.com/a", "https://example.com/b"],
|
|
49
|
-
pages: [
|
|
50
|
+
pages: [page("/one"), page("/two")],
|
|
50
51
|
issues: issues
|
|
51
52
|
)
|
|
52
53
|
|
|
@@ -77,7 +78,7 @@ class CrawlscopeReporterTest < Minitest::Test
|
|
|
77
78
|
base_url: "https://example.com",
|
|
78
79
|
sitemap_path: "/tmp/sitemap.xml",
|
|
79
80
|
urls: ["https://example.com/a"],
|
|
80
|
-
pages: [
|
|
81
|
+
pages: [page],
|
|
81
82
|
issues: issues
|
|
82
83
|
)
|
|
83
84
|
|
|
@@ -107,7 +108,7 @@ class CrawlscopeReporterTest < Minitest::Test
|
|
|
107
108
|
base_url: "https://example.com",
|
|
108
109
|
sitemap_path: "/tmp/sitemap.xml",
|
|
109
110
|
urls: ["https://example.com"],
|
|
110
|
-
pages: [
|
|
111
|
+
pages: [page],
|
|
111
112
|
issues: issues
|
|
112
113
|
)
|
|
113
114
|
|
|
@@ -137,7 +138,7 @@ class CrawlscopeReporterTest < Minitest::Test
|
|
|
137
138
|
base_url: "https://example.com",
|
|
138
139
|
sitemap_path: "/tmp/sitemap.xml",
|
|
139
140
|
urls: ["https://example.com/a"],
|
|
140
|
-
pages: [
|
|
141
|
+
pages: [page],
|
|
141
142
|
issues: issues
|
|
142
143
|
)
|
|
143
144
|
|
|
@@ -164,7 +165,7 @@ class CrawlscopeReporterTest < Minitest::Test
|
|
|
164
165
|
base_url: "https://example.com",
|
|
165
166
|
sitemap_path: "/tmp/sitemap.xml",
|
|
166
167
|
urls: ["https://example.com"],
|
|
167
|
-
pages: [
|
|
168
|
+
pages: [page],
|
|
168
169
|
issues: issues
|
|
169
170
|
)
|
|
170
171
|
|
|
@@ -176,4 +177,61 @@ class CrawlscopeReporterTest < Minitest::Test
|
|
|
176
177
|
assert_includes output, " - /overview-1 indexable internal page is missing from sitemap source: /source-1"
|
|
177
178
|
assert_includes output, " - /overview-4 indexable internal page is missing from sitemap source: /source-4"
|
|
178
179
|
end
|
|
180
|
+
|
|
181
|
+
def test_reports_server_timing_coverage_percentiles_signals_and_worst_pages
|
|
182
|
+
io = StringIO.new
|
|
183
|
+
result = Crawlscope::Result.new(
|
|
184
|
+
base_url: "https://example.com",
|
|
185
|
+
sitemap_path: "/tmp/sitemap.xml",
|
|
186
|
+
urls: [
|
|
187
|
+
"https://example.com/fast",
|
|
188
|
+
"https://example.com/slow",
|
|
189
|
+
"https://example.com/malformed"
|
|
190
|
+
],
|
|
191
|
+
pages: [
|
|
192
|
+
page(
|
|
193
|
+
"/fast",
|
|
194
|
+
server_timing: 'total;dur=100, db;dur=20;desc="Primary", cache;desc="HIT"'
|
|
195
|
+
),
|
|
196
|
+
page(
|
|
197
|
+
"/slow",
|
|
198
|
+
server_timing: 'total;dur=300, db;dur=80, cache;desc="MISS"'
|
|
199
|
+
),
|
|
200
|
+
page("/malformed", server_timing: "bad metric;dur=10")
|
|
201
|
+
],
|
|
202
|
+
issues: Crawlscope::IssueCollection.new
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
Crawlscope::Reporter.new(io: io).report(result)
|
|
206
|
+
|
|
207
|
+
output = io.string
|
|
208
|
+
assert_includes output, "Server Timing:"
|
|
209
|
+
assert_includes output, "Coverage: 3/3 pages (100.0%); durations on 2 pages"
|
|
210
|
+
assert_includes output, "Duration metrics (dur; milliseconds assumed):"
|
|
211
|
+
assert_includes output, "total: 2 samples / 2 pages; avg 200ms; p50 200ms; p95 290ms; max 300ms"
|
|
212
|
+
assert_includes output, 'db "Primary": 1 sample / 1 page'
|
|
213
|
+
assert_includes output, 'cache "HIT": 1 sample / 1 page'
|
|
214
|
+
assert_includes output, "Worst pages:"
|
|
215
|
+
assert_includes output, "/slow: 300ms (total)"
|
|
216
|
+
assert_includes output, "Ignored malformed entries: 1"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
def page(path = "/", server_timing: nil)
|
|
222
|
+
url = "https://example.com#{path}"
|
|
223
|
+
headers = {}
|
|
224
|
+
headers["Server-Timing"] = server_timing unless server_timing.nil?
|
|
225
|
+
|
|
226
|
+
Crawlscope::Page.new(
|
|
227
|
+
url: url,
|
|
228
|
+
normalized_url: url,
|
|
229
|
+
final_url: url,
|
|
230
|
+
normalized_final_url: url,
|
|
231
|
+
status: 200,
|
|
232
|
+
headers: headers,
|
|
233
|
+
body: "",
|
|
234
|
+
doc: nil
|
|
235
|
+
)
|
|
236
|
+
end
|
|
179
237
|
end
|
data/test/crawlscope/run_test.rb
CHANGED
|
@@ -130,29 +130,21 @@ class CrawlscopeRunTest < Minitest::Test
|
|
|
130
130
|
)
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
def
|
|
133
|
+
def test_validate_uses_http_sitemap_for_localhost
|
|
134
134
|
result = FakeResult.new(reported: true)
|
|
135
135
|
configuration = FakeConfiguration.new(result: result, base_url: "http://localhost:3000", sitemap_path: nil)
|
|
136
136
|
reporter = FakeReporter.new
|
|
137
|
-
tmp_dir = Dir.mktmpdir
|
|
138
|
-
sitemap_path = File.join(tmp_dir, "public", "sitemap.xml")
|
|
139
|
-
FileUtils.mkdir_p(File.dirname(sitemap_path))
|
|
140
|
-
File.write(sitemap_path, "<urlset></urlset>")
|
|
141
137
|
|
|
142
|
-
|
|
143
|
-
Crawlscope::Run.new(configuration: configuration, reporter: reporter).validate
|
|
144
|
-
end
|
|
138
|
+
Crawlscope::Run.new(configuration: configuration, reporter: reporter).validate
|
|
145
139
|
|
|
146
140
|
assert_equal(
|
|
147
141
|
{
|
|
148
142
|
base_url: "http://localhost:3000",
|
|
149
|
-
sitemap_path:
|
|
143
|
+
sitemap_path: "http://localhost:3000/sitemap.xml",
|
|
150
144
|
rule_names: nil
|
|
151
145
|
},
|
|
152
146
|
configuration.received_arguments
|
|
153
147
|
)
|
|
154
|
-
ensure
|
|
155
|
-
FileUtils.rm_rf(tmp_dir) if tmp_dir
|
|
156
148
|
end
|
|
157
149
|
|
|
158
150
|
def test_validate_json_ld_reports_valid_structured_data
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class CrawlscopeServerTimingSummaryTest < Minitest::Test
|
|
6
|
+
def test_aggregates_coverage_percentiles_signals_and_worst_pages
|
|
7
|
+
pages = [
|
|
8
|
+
page("/fast", 'total;dur=100, db;dur=20, cache;desc="HIT"'),
|
|
9
|
+
page("/typical", 'total;dur=200, db;dur=50, cache;desc="MISS"'),
|
|
10
|
+
page("/slow", 'total;dur=300, db;dur=80, cache;desc="HIT", miss'),
|
|
11
|
+
page("/no-header"),
|
|
12
|
+
page("/malformed", "bad metric;dur=10")
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
summary = Crawlscope::ServerTiming::Summary.new(pages)
|
|
16
|
+
|
|
17
|
+
assert summary.present?
|
|
18
|
+
assert_equal 5, summary.pages.size
|
|
19
|
+
assert_equal 4, summary.coverage
|
|
20
|
+
assert_equal 3, summary.duration_pages
|
|
21
|
+
assert_equal 1, summary.invalid_count
|
|
22
|
+
|
|
23
|
+
total = summary.metrics.find { |metric| metric[:name] == "total" }
|
|
24
|
+
assert_equal 3, total[:samples]
|
|
25
|
+
assert_equal 3, total[:pages]
|
|
26
|
+
assert_in_delta 200.0, total[:average]
|
|
27
|
+
assert_in_delta 200.0, total[:p50]
|
|
28
|
+
assert_in_delta 290.0, total[:p95]
|
|
29
|
+
assert_in_delta 300.0, total[:maximum]
|
|
30
|
+
|
|
31
|
+
hit = summary.signals.find do |signal|
|
|
32
|
+
signal[:name] == "cache" && signal[:description] == "HIT"
|
|
33
|
+
end
|
|
34
|
+
assert_equal 2, hit[:samples]
|
|
35
|
+
assert_equal 2, hit[:pages]
|
|
36
|
+
|
|
37
|
+
assert_equal(
|
|
38
|
+
[
|
|
39
|
+
["https://example.com/slow", "total", 300.0],
|
|
40
|
+
["https://example.com/typical", "total", 200.0]
|
|
41
|
+
],
|
|
42
|
+
summary.worst_pages(limit: 2).map do |sample|
|
|
43
|
+
[sample[:page].url, sample[:metric].name, sample[:metric].duration]
|
|
44
|
+
end
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_is_absent_when_no_pages_publish_the_header
|
|
49
|
+
summary = Crawlscope::ServerTiming::Summary.new([page("/one"), page("/two")])
|
|
50
|
+
|
|
51
|
+
refute summary.present?
|
|
52
|
+
assert_empty summary.metrics
|
|
53
|
+
assert_empty summary.signals
|
|
54
|
+
assert_empty summary.worst_pages
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_returns_ten_worst_pages_by_default
|
|
58
|
+
pages = 12.times.map do |index|
|
|
59
|
+
page("/page-#{index + 1}", "total;dur=#{index + 1}")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
summary = Crawlscope::ServerTiming::Summary.new(pages)
|
|
63
|
+
|
|
64
|
+
assert_equal 10, summary.worst_pages.size
|
|
65
|
+
assert_equal "https://example.com/page-12", summary.worst_pages.first[:page].url
|
|
66
|
+
assert_equal "https://example.com/page-3", summary.worst_pages.last[:page].url
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def page(path, server_timing = nil)
|
|
72
|
+
url = "https://example.com#{path}"
|
|
73
|
+
headers = {}
|
|
74
|
+
headers["Server-Timing"] = server_timing unless server_timing.nil?
|
|
75
|
+
|
|
76
|
+
Crawlscope::Page.new(
|
|
77
|
+
url: url,
|
|
78
|
+
normalized_url: url,
|
|
79
|
+
final_url: url,
|
|
80
|
+
normalized_final_url: url,
|
|
81
|
+
status: 200,
|
|
82
|
+
headers: headers,
|
|
83
|
+
body: "",
|
|
84
|
+
doc: nil
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class CrawlscopeServerTimingTest < Minitest::Test
|
|
6
|
+
def test_parses_metrics_durations_descriptions_and_unknown_parameters
|
|
7
|
+
timing = Crawlscope::ServerTiming.new(
|
|
8
|
+
'miss, db;dur=53, cache;desc="Cache Read";dur=23.2;region=atl'
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
assert timing.present?
|
|
12
|
+
assert_equal 0, timing.invalid_count
|
|
13
|
+
assert_equal %w[miss db cache], timing.map(&:name)
|
|
14
|
+
|
|
15
|
+
cache = timing.to_a.last
|
|
16
|
+
assert_equal 23.2, cache.duration
|
|
17
|
+
assert_equal "Cache Read", cache.description
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_preserves_duplicate_metrics_and_uses_the_first_duplicate_parameter
|
|
21
|
+
timing = Crawlscope::ServerTiming.new(
|
|
22
|
+
["db;DUR=10;dur=20", "db;dur=30"]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
assert_equal [10.0, 30.0], timing.map(&:duration)
|
|
26
|
+
assert_equal 0, timing.invalid_count
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_parses_rails_notification_names
|
|
30
|
+
timing = Crawlscope::ServerTiming.new(
|
|
31
|
+
"sql.active_record;dur=82.4, " \
|
|
32
|
+
"render_template.action_view;dur=10.2, " \
|
|
33
|
+
"process_action.action_controller;dur=101.5"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
assert_equal(
|
|
37
|
+
%w[
|
|
38
|
+
sql.active_record
|
|
39
|
+
render_template.action_view
|
|
40
|
+
process_action.action_controller
|
|
41
|
+
],
|
|
42
|
+
timing.map(&:name)
|
|
43
|
+
)
|
|
44
|
+
assert_equal 0, timing.invalid_count
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_handles_delimiters_and_escapes_inside_quoted_descriptions
|
|
48
|
+
timing = Crawlscope::ServerTiming.new(
|
|
49
|
+
'cache;desc="Cache, read; \\"fast\\"";dur=23.2'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert_equal 1, timing.size
|
|
53
|
+
assert_equal 'Cache, read; "fast"', timing.first.description
|
|
54
|
+
assert_equal 0, timing.invalid_count
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_reports_invalid_entries_without_discarding_valid_metrics
|
|
58
|
+
timing = Crawlscope::ServerTiming.new(
|
|
59
|
+
"bad metric;dur=2, db;dur=nope, app;dur=3, cache;broken"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert_equal %w[app cache], timing.map(&:name)
|
|
63
|
+
assert_equal 3.0, timing.first.duration
|
|
64
|
+
assert_equal 2, timing.invalid_count
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_rejects_malformed_standard_parameters
|
|
68
|
+
timing = Crawlscope::ServerTiming.new(
|
|
69
|
+
"db;dur, cache;desc=not quoted, app;dur=3"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
assert_equal ["app"], timing.map(&:name)
|
|
73
|
+
assert_equal 2, timing.invalid_count
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_keeps_complete_metrics_before_an_unterminated_description
|
|
77
|
+
timing = Crawlscope::ServerTiming.new(
|
|
78
|
+
'db;dur=5, cache;desc="unterminated'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
assert_equal ["db"], timing.map(&:name)
|
|
82
|
+
assert_equal 1, timing.invalid_count
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_ignores_empty_list_members
|
|
86
|
+
timing = Crawlscope::ServerTiming.new(", db;dur=5, , app;dur=3,")
|
|
87
|
+
|
|
88
|
+
assert_equal %w[db app], timing.map(&:name)
|
|
89
|
+
assert_equal 0, timing.invalid_count
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_distinguishes_an_absent_header_from_an_empty_header
|
|
93
|
+
refute Crawlscope::ServerTiming.new(nil).present?
|
|
94
|
+
|
|
95
|
+
timing = Crawlscope::ServerTiming.new("")
|
|
96
|
+
|
|
97
|
+
assert timing.present?
|
|
98
|
+
assert_empty timing
|
|
99
|
+
assert_equal 0, timing.invalid_count
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -34,6 +34,27 @@ class CrawlscopeSitemapTest < Minitest::Test
|
|
|
34
34
|
assert_equal ["https://www.example.com/", "https://www.example.com/pricing"], parser.urls(base_url: "https://www.example.com")
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def test_remote_sitemap_sends_profile_token
|
|
38
|
+
request = stub_request(:get, "https://www.example.com/sitemap.xml")
|
|
39
|
+
.with(headers: {"X-Profile-Token" => "profile-token"})
|
|
40
|
+
.to_return(
|
|
41
|
+
status: 200,
|
|
42
|
+
body: <<~XML
|
|
43
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
44
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
45
|
+
<url><loc>https://www.example.com/</loc></url>
|
|
46
|
+
</urlset>
|
|
47
|
+
XML
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
Crawlscope::Sitemap.new(
|
|
51
|
+
path: "https://www.example.com/sitemap.xml",
|
|
52
|
+
profile_token: "profile-token"
|
|
53
|
+
).urls(base_url: "https://www.example.com")
|
|
54
|
+
|
|
55
|
+
assert_requested request
|
|
56
|
+
end
|
|
57
|
+
|
|
37
58
|
def test_parses_remote_sitemap_index_with_child_sitemap
|
|
38
59
|
stub_request(:get, "https://www.example.com/sitemap.xml")
|
|
39
60
|
.to_return(
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: crawlscope
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paulo Fidalgo
|
|
@@ -238,6 +238,7 @@ files:
|
|
|
238
238
|
- lib/crawlscope/railtie.rb
|
|
239
239
|
- lib/crawlscope/rake_tasks.rb
|
|
240
240
|
- lib/crawlscope/reporter.rb
|
|
241
|
+
- lib/crawlscope/request_headers.rb
|
|
241
242
|
- lib/crawlscope/result.rb
|
|
242
243
|
- lib/crawlscope/rule_registry.rb
|
|
243
244
|
- lib/crawlscope/rules/content_quality.rb
|
|
@@ -249,6 +250,9 @@ files:
|
|
|
249
250
|
- lib/crawlscope/run.rb
|
|
250
251
|
- lib/crawlscope/schema_registry.rb
|
|
251
252
|
- lib/crawlscope/schemas.rb
|
|
253
|
+
- lib/crawlscope/server_timing.rb
|
|
254
|
+
- lib/crawlscope/server_timing/reporter.rb
|
|
255
|
+
- lib/crawlscope/server_timing/summary.rb
|
|
252
256
|
- lib/crawlscope/sitemap.rb
|
|
253
257
|
- lib/crawlscope/structured_data/audit.rb
|
|
254
258
|
- lib/crawlscope/structured_data/check.rb
|
|
@@ -281,6 +285,8 @@ files:
|
|
|
281
285
|
- test/crawlscope/rule_registry_test.rb
|
|
282
286
|
- test/crawlscope/run_test.rb
|
|
283
287
|
- test/crawlscope/schema_registry_test.rb
|
|
288
|
+
- test/crawlscope/server_timing_summary_test.rb
|
|
289
|
+
- test/crawlscope/server_timing_test.rb
|
|
284
290
|
- test/crawlscope/sitemap_test.rb
|
|
285
291
|
- test/crawlscope/structured_data_audit_test.rb
|
|
286
292
|
- test/crawlscope/structured_data_document_test.rb
|