chrome_debugger 0.0.1

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.
data/CHANGELOG ADDED
@@ -0,0 +1,2 @@
1
+ v0.0.1 (XXX)
2
+ - initial release
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Chrome Debugger
2
+
3
+ Ever wanted to capture stats (#requests, onload time, etc.) about the state of your frontend? Us too!
4
+ Chrome Debugger uses the remote debugging protocol in Chrome to do just that!
5
+
6
+ Intended to be a used in a post-deploy or CI step.
7
+
8
+ Yay-hooray!
9
+
10
+ ## Installation
11
+
12
+ gem install chrome_debugger
13
+
14
+ ## Extra Requirements
15
+
16
+ Chrome 18 or higher must be installed and available on the path.
17
+
18
+ ## Usage
19
+
20
+ require 'chrome_debugger'
21
+
22
+ ChromeDebugger::Client.open do |chrome|
23
+ document = chrome.load_url("https://theconversation.edu.au/")
24
+
25
+ puts "requests: #{document.request_count}"
26
+ puts "onload_event: #{document.onload_event}"
27
+ puts "dom_content_event: #{document.dom_content_event}"
28
+ puts "document_payload: #{document.encoded_bytes("Document")}"
29
+ puts "script_payload: #{document.encoded_bytes("Script")}"
30
+ puts "image_payload: #{document.encoded_bytes("Image")}"
31
+ end
32
+
33
+ Refer to the ChromeDebugger::Client and ChromeDebugger::Document classes for
34
+ detailed docs.
35
+
36
+ ChromeDebugger::Client starts and manages a new chrome session.
37
+
38
+ ChromeDebugger::Document provides an entry point for querying the results of
39
+ a page load.
40
+
41
+ ## Authors
42
+
43
+ Justin Morris
44
+ justin.morris@theconversation.edu.au
45
+
46
+ James Healy
47
+ james.healy@theconversation.edu.au
48
+
49
+ ## Further Reading
50
+
51
+ * https://developers.google.com/chrome-developer-tools/docs/remote-debugging
52
+ * http://www.igvita.com/2012/04/09/driving-google-chrome-via-websocket-api/
53
+
54
+ ## TODO
55
+
56
+ Possible further work.
57
+
58
+ * make the chrome path configurable
59
+ * make headless mode configurable
data/examples/basic.rb ADDED
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+
3
+ # A basic example that prints stats on a website to STDOUT.
4
+ #
5
+ # Usage:
6
+ #
7
+ # ruby basic.rb
8
+
9
+ $:.unshift File.expand_path('../../lib/', __FILE__)
10
+ require 'chrome_debugger'
11
+
12
+ ChromeDebugger::Client.open do |chrome|
13
+ document = chrome.load_url("https://theconversation.edu.au/")
14
+ {
15
+ requests: document.request_count,
16
+ onload_event: document.onload_event,
17
+ dom_content_event: document.dom_content_event,
18
+ document_payload: document.encoded_bytes("Document"),
19
+ script_payload: document.encoded_bytes("Script"),
20
+ image_payload: document.encoded_bytes("Image"),
21
+ stylesheet_payload: document.encoded_bytes("Stylesheet"),
22
+ other_payload: document.encoded_bytes("Other"),
23
+ document_uncompressed_payload: document.bytes("Document"),
24
+ script_uncompressed_payload: document.bytes("Script"),
25
+ image_uncompressed_payload: document.bytes("Image"),
26
+ stylesheet_uncompressed_payload: document.bytes("Stylesheet"),
27
+ other_uncompressed_payload: document.bytes("Other"),
28
+ script_count: document.request_count_by_resource("Script"),
29
+ image_count: document.request_count_by_resource("Image"),
30
+ stylesheet_count: document.request_count_by_resource("Stylesheet")
31
+ }.each {|key, value|
32
+ puts "#{key}: #{value}"
33
+ }
34
+ end
@@ -0,0 +1,57 @@
1
+ # coding: utf-8
2
+
3
+ # Demonstrates how we use ChromeDebugger at @ConversationEdu. We track various
4
+ # performance metrics on key pages and send the results to Librato Metrics, a
5
+ # hosted metrics platform.
6
+ #
7
+ # Usage:
8
+ #
9
+ # LIBRATO_USER=someone@example.com LIBRATO_KEY=SECRET ruby theconversation.rb
10
+
11
+ require 'chrome_debugger'
12
+ require 'librato/metrics'
13
+
14
+ # Config
15
+
16
+ LIBRATO_PREFIX = "tc.frontend"
17
+ LIBRATO_USER = ENV["LIBRATO_USER"]
18
+ LIBRATO_KEY = ENV["LIBRATO_KEY"]
19
+ PAGES = {
20
+ homepage: 'https://theconversation.edu.au/',
21
+ articlepage: 'https://theconversation.edu.au/bike-lanes-economic-benefits-go-beyond-jobs-6081/'
22
+ }
23
+
24
+ # ACTION
25
+
26
+ Librato::Metrics.authenticate(LIBRATO_USER, LIBRATO_KEY)
27
+ librato_queue = Librato::Metrics::Queue.new
28
+
29
+ PAGES.each do |name, url|
30
+ ChromeDebugger::Client.open do |chrome|
31
+ document = chrome.load_url(url)
32
+ {
33
+ requests: document.request_count,
34
+ onload_event: document.onload_event,
35
+ dom_content_event: document.dom_content_event,
36
+ document_payload: document.encoded_bytes("Document"),
37
+ script_payload: document.encoded_bytes("Script"),
38
+ image_payload: document.encoded_bytes("Image"),
39
+ stylesheet_payload: document.encoded_bytes("Stylesheet"),
40
+ other_payload: document.encoded_bytes("Other"),
41
+ document_uncompressed_payload: document.bytes("Document"),
42
+ script_uncompressed_payload: document.bytes("Script"),
43
+ image_uncompressed_payload: document.bytes("Image"),
44
+ stylesheet_uncompressed_payload: document.bytes("Stylesheet"),
45
+ other_uncompressed_payload: document.bytes("Other"),
46
+ script_count: document.request_count_by_resource("Script"),
47
+ image_count: document.request_count_by_resource("Image"),
48
+ stylesheet_count: document.request_count_by_resource("Stylesheet")
49
+ }.each {|key, value|
50
+ puts "#{LIBRATO_PREFIX}.#{name}.#{key}: #{value}"
51
+ }.each { |key, value|
52
+ librato_queue.add("#{LIBRATO_PREFIX}.#{name}.#{key}" => value)
53
+ }
54
+ end
55
+ end
56
+
57
+ librato_queue.submit
@@ -0,0 +1 @@
1
+ require 'chrome_debugger/client'
@@ -0,0 +1,136 @@
1
+ require 'em-http'
2
+ require 'faye/websocket'
3
+ require 'headless'
4
+ require 'json'
5
+
6
+ require 'securerandom'
7
+
8
+ require 'chrome_debugger/document'
9
+ require 'chrome_debugger/notification'
10
+ require 'chrome_debugger/data_received'
11
+ require 'chrome_debugger/dom_content_event_fired'
12
+ require 'chrome_debugger/load_event_fired'
13
+ require 'chrome_debugger/request_will_be_sent'
14
+ require 'chrome_debugger/response_received'
15
+
16
+ module ChromeDebugger
17
+ class Client
18
+
19
+ PAGE_LOAD_WAIT = 16
20
+ REMOTE_DEBUGGING_PORT = 9222
21
+
22
+ def initialize(opts = {})
23
+ @chrome_path = find_chrome_binary
24
+ end
25
+
26
+ def self.open(&block)
27
+ headless = Headless.new
28
+ headless.start
29
+ chrome = ChromeDebugger::Client.new
30
+ chrome.start_chrome
31
+ yield chrome
32
+ ensure
33
+ chrome.cleanup
34
+ headless.destroy
35
+ end
36
+
37
+ def start_chrome
38
+ @profile_dir = File.join(Dir.tmpdir, SecureRandom.hex(10))
39
+ @chrome_cmd = "'#{@chrome_path}' --user-data-dir=#{@profile_dir} -remote-debugging-port=#{REMOTE_DEBUGGING_PORT} --no-first-run"
40
+ @chrome_pid = Process.spawn(@chrome_cmd, :pgroup => true)
41
+
42
+ until debug_port_listening?
43
+ sleep 0.1
44
+ end
45
+ end
46
+
47
+ def load_url(url)
48
+ raise "call the start_chrome() method first" unless @chrome_pid
49
+ document = ChromeDebugger::Document.new(url)
50
+ load(document)
51
+ document
52
+ end
53
+
54
+ def cleanup
55
+ if @chrome_pid
56
+ Process.kill('-TERM', Process.getpgid(@chrome_pid))
57
+ sleep 3
58
+ FileUtils.rm_rf(@profile_dir) if @profile_dir && File.directory?(@profile_dir)
59
+ @chrome_pid = nil
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def find_chrome_binary
66
+ path = [
67
+ "/usr/bin/google-chrome",
68
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
69
+ ].detect { |path|
70
+ File.file?(path)
71
+ }
72
+ raise "No Chrome binary found" if path.nil?
73
+ path
74
+ end
75
+
76
+ def handle_data(document, data)
77
+ unless data['result']
78
+ case data['method']
79
+ when "Network.requestWillBeSent" then
80
+ # The browser is initiating a new HTTP request
81
+ document.events << ChromeDebugger::RequestWillBeSent.new(data)
82
+ when "Page.domContentEventFired" then
83
+ document.events << ChromeDebugger::DomContentEventFired.new(data)
84
+ when "Page.loadEventFired" then
85
+ document.events << ChromeDebugger::LoadEventFired.new(data)
86
+ when "Network.responseReceived" then
87
+ document.events << ChromeDebugger::ResponseReceived.new(data)
88
+ when "Network.dataReceived" then
89
+ document.events << ChromeDebugger::DataReceived.new(data)
90
+ else
91
+ document.events << ChromeDebugger::Notification.new(data)
92
+ end
93
+ end
94
+ end
95
+
96
+ def load(document)
97
+ EM.run do
98
+ EM::add_periodic_timer(0.5) do
99
+ EM.stop_event_loop if document.onload_event
100
+ end
101
+
102
+ conn = EM::HttpRequest.new("http://localhost:#{REMOTE_DEBUGGING_PORT}/json").get
103
+ conn.callback do
104
+ response = JSON.parse(conn.response)
105
+
106
+ ws = Faye::WebSocket::Client.new response.first['webSocketDebuggerUrl']
107
+
108
+ ws.onmessage = lambda do |message|
109
+ data = JSON.parse(message.data)
110
+ handle_data(document, data)
111
+ end
112
+
113
+ ws.onopen = lambda do |event|
114
+ ws.send JSON.dump({id: 1, method: 'Page.enable'})
115
+ ws.send JSON.dump({id: 2, method: 'Network.enable'})
116
+ ws.send JSON.dump({id: 3, method: 'Network.setCacheDisabled', params: {cacheDisabled: true}})
117
+ ws.send JSON.dump({id: 4, method: 'Network.clearBrowserCache'})
118
+ ws.send JSON.dump({
119
+ id: 5,
120
+ method: 'Page.navigate',
121
+ params: {url: document.url}
122
+ })
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ def debug_port_listening?
129
+ TCPSocket.new('localhost', REMOTE_DEBUGGING_PORT).close
130
+ true
131
+ rescue Errno::ECONNREFUSED
132
+ false
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,19 @@
1
+ require 'chrome_debugger/notification'
2
+
3
+ module ChromeDebugger
4
+ class DataReceived < Notification
5
+
6
+ # uncompressed bytes in the HTTP content. Excludes HTTP headers
7
+ #
8
+ def data_length
9
+ @params['dataLength'].to_i
10
+ end
11
+
12
+ # bytes received over the wire. Includes HTTP headers
13
+ # and HTTP content. The HTTP content may be compressed.
14
+ #
15
+ def encoded_data_length
16
+ @params['encodedDataLength'].to_i
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,119 @@
1
+ require 'chrome_debugger/dom_content_event_fired'
2
+ require 'chrome_debugger/load_event_fired'
3
+ require 'chrome_debugger/notification'
4
+ require 'chrome_debugger/response_received'
5
+ require 'chrome_debugger/request_will_be_sent'
6
+
7
+ module ChromeDebugger
8
+ class Document
9
+
10
+ attr_reader :url, :events
11
+
12
+ def initialize(url)
13
+ @url = url
14
+ @timestamp = 0
15
+ @events = []
16
+ end
17
+
18
+ # The seconds since epoch that the request for this document started
19
+ #
20
+ def start_time
21
+ @start_time ||= @events.select { |event|
22
+ event.is_a?(RequestWillBeSent)
23
+ }.select { |event|
24
+ event.request['url'] == @url
25
+ }.map { |event|
26
+ event.timestamp
27
+ }.first
28
+ end
29
+
30
+ # The number of seconds *after* start_time that the OnLoad event fired
31
+ #
32
+ def onload_event
33
+ @onload_event ||= begin
34
+ ts = @events.select { |event|
35
+ event.is_a?(LoadEventFired)
36
+ }.slice(0,1).map(&:timestamp).first
37
+ ts ? (ts - start_time).round(3) : nil
38
+ end
39
+ end
40
+
41
+ # The number of seconds *after* start_time the the DomReady event fired
42
+ #
43
+ def dom_content_event
44
+ @dom_content_event ||= begin
45
+ ts = @events.select { |event|
46
+ event.is_a?(DomContentEventFired)
47
+ }.slice(0,1).map(&:timestamp).first
48
+ ts ? (ts - start_time).round(3) : nil
49
+ end
50
+ end
51
+
52
+ # The number of bytes downloaded for a particular resource type. If the
53
+ # resource was gzipped during transfer then the gzipped size is reported.
54
+ #
55
+ # The HTTP headers for the response are included in the byte count.
56
+ #
57
+ # Possible resource types: 'Document','Script', 'Image', 'Stylesheet',
58
+ # 'Other'.
59
+ #
60
+ def encoded_bytes(resource_type)
61
+ @events.select {|e|
62
+ e.is_a?(ResponseReceived) && e.resource_type == resource_type
63
+ }.map { |e|
64
+ e.request_id
65
+ }.map { |request_id|
66
+ data_received_for_request(request_id)
67
+ }.flatten.inject(0) { |bytes_sum, n| bytes_sum + n.encoded_data_length }
68
+ end
69
+
70
+ # The number of bytes downloaded for a particular resource type. If the
71
+ # resource was gzipped during transfer then the uncompressed size is
72
+ # reported.
73
+ #
74
+ # The HTTP headers for the response are NOT included in the byte count.
75
+ #
76
+ # Possible resource types: 'Document','Script', 'Image', 'Stylesheet',
77
+ # 'Other'.
78
+ #
79
+ def bytes(resource_type)
80
+ @events.select {|e|
81
+ e.is_a?(ResponseReceived) && e.resource_type == resource_type
82
+ }.map { |e|
83
+ e.request_id
84
+ }.map { |request_id|
85
+ data_received_for_request(request_id)
86
+ }.flatten.inject(0) { |bytes_sum, n| bytes_sum + n.data_length }
87
+ end
88
+
89
+ # The number of network requests required to load this document
90
+ #
91
+ def request_count
92
+ @events.select {|n|
93
+ n.is_a?(ResponseReceived)
94
+ }.size
95
+ end
96
+
97
+ # the number of network requests of a particular resource
98
+ # type that were required to load this document
99
+ #
100
+ # Possible resource types: 'Document', 'Script', 'Image', 'Stylesheet',
101
+ # 'Other'.
102
+ #
103
+ def request_count_by_resource(resource_type)
104
+ @events.select {|n|
105
+ n.is_a?(ResponseReceived) && n.resource_type == resource_type
106
+ }.size
107
+ end
108
+
109
+ private
110
+
111
+ def data_received_for_request(id)
112
+ @events.select { |e|
113
+ e.is_a?(DataReceived) && e.request_id == id
114
+ }
115
+ end
116
+
117
+
118
+ end
119
+ end
@@ -0,0 +1,10 @@
1
+ require 'chrome_debugger/notification'
2
+
3
+ module ChromeDebugger
4
+ class DomContentEventFired < Notification
5
+
6
+ def timestamp
7
+ @params['timestamp'].to_f
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ require 'chrome_debugger/notification'
2
+
3
+ module ChromeDebugger
4
+ class LoadEventFired < Notification
5
+
6
+
7
+ def timestamp
8
+ @params['timestamp'].to_f
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module ChromeDebugger
2
+ class Notification
3
+
4
+ attr_reader :method
5
+
6
+ def initialize(point)
7
+ @params = point['params'] || {}
8
+ @method = point['method']
9
+ end
10
+
11
+ def resource_type
12
+ @params['type']
13
+ end
14
+
15
+ def request_id
16
+ @params['requestId']
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ require 'chrome_debugger/notification'
2
+
3
+ module ChromeDebugger
4
+ class RequestWillBeSent < Notification
5
+
6
+ def request
7
+ @params['request']
8
+ end
9
+
10
+ def timestamp
11
+ @params['timestamp'].to_f
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ require 'chrome_debugger/notification'
2
+
3
+ module ChromeDebugger
4
+ class ResponseReceived < Notification
5
+
6
+ def bytes
7
+ @params['response']['headers']['Content-Length'].to_i
8
+ end
9
+
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chrome_debugger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Justin Morris
9
+ - James Healy
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-08-27 00:00:00.000000000 Z
14
+ dependencies: []
15
+ description: Starts a Google Chrome session. Load pages and examine the results.
16
+ email:
17
+ - justin.morris@theconversation.edu.au
18
+ - james.healy@theconversation.edu.au
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - examples/basic.rb
24
+ - examples/theconversation.rb
25
+ - lib/chrome_debugger/client.rb
26
+ - lib/chrome_debugger/data_received.rb
27
+ - lib/chrome_debugger/document.rb
28
+ - lib/chrome_debugger/dom_content_event_fired.rb
29
+ - lib/chrome_debugger/load_event_fired.rb
30
+ - lib/chrome_debugger/notification.rb
31
+ - lib/chrome_debugger/request_will_be_sent.rb
32
+ - lib/chrome_debugger/response_received.rb
33
+ - lib/chrome_debugger.rb
34
+ - README.md
35
+ - CHANGELOG
36
+ homepage: http://github.com/conversation/chrome_debugger
37
+ licenses: []
38
+ post_install_message:
39
+ rdoc_options:
40
+ - --title
41
+ - Chrome Debugger
42
+ - --line-numbers
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: 1.9.2
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: 1.3.2
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 1.8.24
60
+ signing_key:
61
+ specification_version: 3
62
+ summary: Remotely control Google Chrome and extract stats
63
+ test_files: []