chrome_debugger 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []