-
Notifications
You must be signed in to change notification settings - Fork 599
/
browser_monitoring.rb
156 lines (130 loc) · 5.14 KB
/
browser_monitoring.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# encoding: utf-8
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details.
require 'rack'
require 'new_relic/rack/agent_middleware'
require 'new_relic/agent/instrumentation/middleware_proxy'
module NewRelic::Rack
# This middleware is used by the agent for the Real user monitoring (RUM)
# feature, and will usually be automatically injected in the middleware chain.
# If automatic injection is not working, you may manually use it in your
# middleware chain instead.
#
# @api public
#
class BrowserMonitoring < AgentMiddleware
# The maximum number of bytes of the response body that we will
# examine in order to look for a RUM insertion point.
SCAN_LIMIT = 50_000
CONTENT_TYPE = 'Content-Type'.freeze
CONTENT_DISPOSITION = 'Content-Disposition'.freeze
CONTENT_LENGTH = 'Content-Length'.freeze
ATTACHMENT = 'attachment'.freeze
TEXT_HTML = 'text/html'.freeze
BODY_START = "<body".freeze
HEAD_START = "<head".freeze
GT = ">".freeze
def traced_call(env)
result = @app.call(env)
(status, headers, response) = result
js_to_inject = NewRelic::Agent.browser_timing_header
if (js_to_inject != "") && should_instrument?(env, status, headers)
response_string = autoinstrument_source(response, headers, js_to_inject)
if headers.key?(CONTENT_LENGTH)
headers[CONTENT_LENGTH] = response_string.size.to_s
end
env[ALREADY_INSTRUMENTED_KEY] = true
if response_string
response = Rack::Response.new(response_string, status, headers)
response.finish
else
result
end
else
result
end
end
ALREADY_INSTRUMENTED_KEY = "newrelic.browser_monitoring_already_instrumented"
def should_instrument?(env, status, headers)
NewRelic::Agent.config[:'browser_monitoring.auto_instrument'] &&
status == 200 &&
!env[ALREADY_INSTRUMENTED_KEY] &&
is_html?(headers) &&
!is_attachment?(headers) &&
!is_streaming?(env, headers)
end
def is_html?(headers)
headers[CONTENT_TYPE] && headers[CONTENT_TYPE].include?(TEXT_HTML)
end
def is_attachment?(headers)
headers[CONTENT_DISPOSITION] && headers[CONTENT_DISPOSITION].include?(ATTACHMENT)
end
def is_streaming?(env, headers)
return true if headers && headers['Transfer-Encoding'] == 'chunked'
defined?(ActionController::Live) &&
env['action_controller.instance'].class.included_modules.include?(ActionController::Live)
end
CHARSET_RE = /<\s*meta[^>]+charset\s*=[^>]*>/im.freeze
X_UA_COMPATIBLE_RE = /<\s*meta[^>]+http-equiv\s*=\s*['"]x-ua-compatible['"][^>]*>/im.freeze
def autoinstrument_source(response, headers, js_to_inject)
source = gather_source(response)
close_old_response(response)
return nil unless source
# Only scan the first 50k (roughly) then give up.
beginning_of_source = source[0..SCAN_LIMIT]
if body_start = find_body_start(beginning_of_source)
meta_tag_positions = [
find_x_ua_compatible_position(beginning_of_source),
find_charset_position(beginning_of_source)
].compact
if !meta_tag_positions.empty?
insertion_index = meta_tag_positions.max
else
insertion_index = find_end_of_head_open(beginning_of_source) || body_start
end
if insertion_index
source = source[0...insertion_index] <<
js_to_inject <<
source[insertion_index..-1]
else
NewRelic::Agent.logger.debug "Skipping RUM instrumentation. Could not properly determine location to inject script."
end
else
msg = "Skipping RUM instrumentation. Unable to find <body> tag in first #{SCAN_LIMIT} bytes of document."
NewRelic::Agent.logger.log_once(:warn, :rum_insertion_failure, msg)
NewRelic::Agent.logger.debug(msg)
end
source
rescue => e
NewRelic::Agent.logger.debug "Skipping RUM instrumentation on exception.", e
nil
end
def gather_source(response)
source = nil
response.each {|fragment| source ? (source << fragment.to_s) : (source = fragment.to_s)}
source
end
# Per "The Response > The Body" section of Rack spec, we should close
# if our response is able. http://rack.rubyforge.org/doc/SPEC.html
def close_old_response(response)
if response.respond_to?(:close)
response.close
end
end
def find_body_start(beginning_of_source)
beginning_of_source.index(BODY_START)
end
def find_x_ua_compatible_position(beginning_of_source)
match = X_UA_COMPATIBLE_RE.match(beginning_of_source)
match.end(0) if match
end
def find_charset_position(beginning_of_source)
match = CHARSET_RE.match(beginning_of_source)
match.end(0) if match
end
def find_end_of_head_open(beginning_of_source)
head_open = beginning_of_source.index(HEAD_START)
beginning_of_source.index(GT, head_open) + 1 if head_open
end
end
end