forked from chrissnell/crabby
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathselenium.go
347 lines (281 loc) · 8.65 KB
/
selenium.go
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package main
import (
"bytes"
"context"
"fmt"
"log"
"net/url"
"sync"
"time"
"sourcegraph.com/sourcegraph/go-selenium"
)
// SeleniumJobConfig holds configuration for a selenium job
type SeleniumJobConfig struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
Method string `yaml:"method"`
Interval uint16 `yaml:"interval"`
Tags map[string]string `yaml:"tags,omitempty"`
Cookies []Cookie `yaml:"cookies,omitempty"`
seleniumServer string
}
// GetJobName returns the name of the job
func (c *SeleniumJobConfig) GetJobName() string {
return c.Name
}
// SeleniumJob holds the runtime configuration for a selenium job
type SeleniumJob struct {
config SeleniumJobConfig
wg *sync.WaitGroup
ctx context.Context
storage *Storage
}
// StartJob starts a selenium job
func (j *SeleniumJob) StartJob() {
j.wg.Add(1)
defer j.wg.Done()
log.Println("Starting job", j.config.Name)
jobTicker := time.NewTicker(time.Duration(j.config.Interval) * time.Second)
for {
select {
case <-jobTicker.C:
go j.RunSeleniumTest()
case <-j.ctx.Done():
log.Println("Cancellation request received. Cancelling job runner.")
return
}
}
}
/*
Order of occurance for available timing measurements:
navigationStart -> redirectStart -> redirectEnd -> fetchStart ->
domainLookupStart -> domainLookupEnd -> connectStart -> connectEnd ->
requestStart -> responseStart -> responseEnd -> domLoading ->
domInteractive -> domContentLoaded -> domComplete -> loadEventStart ->
loadEventEnd
*/
type requestTimings struct {
navigationStart float64
redirectStart float64
redirectEnd float64
fetchStart float64
domainLookupStart float64
domainLookupEnd float64
connectStart float64
connectEnd float64
requestStart float64
responseStart float64
responseEnd float64
domLoading float64
domInteractive float64
domContentLoaded float64
domComplete float64
loadEventStart float64
loadEventEnd float64
}
// requestIntervals holds intervals that we derive from requestTimings
type requestIntervals struct {
dnsDuration float64
serverConnectionDuration float64
serverProcessingDuration float64
serverResponseDuration float64
domRenderingDuration float64
timeToFirstByte float64
}
// webRequest is a single test against a web server
type webRequest struct {
url string
rt *requestTimings
ri *requestIntervals
wd selenium.WebDriver
}
// RunSeleniumTest sends a Selenium job to the Selenium service for running and
// calculates timings
func (j *SeleniumJob) RunSeleniumTest() {
var err error
wr := newWebRequest(j.config.URL)
err = wr.setRemote(j.config.seleniumServer)
if err != nil {
log.Println("Error connecting to Selenium service:", err)
return
}
defer wr.wd.Quit()
// There is a security feature with the popular webdrivers (Chrome, Firefox/Gecko,
// and possibly others) that prevents you from setting cookies in Selenium
// when the browser is not already on the domain for which the cookies are
// being set. To work around this, we need to first load a bogus page on
// the same domain (anything that generates a 404 is fine) before attempting
// tos et the cookies.
// We only need to use this work-around if we have cookies to set
if len(j.config.Cookies) > 0 {
var buf bytes.Buffer
var u *url.URL
u, err = url.Parse(j.config.URL)
if err != nil {
log.Printf("Error parsing url %v: %v\n", j.config.URL, err)
return
}
buf.WriteString(u.Scheme)
buf.WriteString("://")
buf.WriteString(u.Host)
buf.WriteString("/selenium-testing-404?source=crabby")
err = wr.wd.Get(buf.String())
if err != nil {
log.Printf("Error fetching page %v: %v\n", buf.String(), err)
return
}
err = wr.AddCookies(j.config.Cookies)
if err != nil {
log.Println("Error adding cookies to Selenium request:", err)
return
}
}
err = wr.wd.Get(wr.url)
if err != nil {
log.Println("Error: Selenium failed to load page:", err)
return
}
err = wr.getTimings()
if err != nil {
log.Println("Error: Could not get page timings from Selenium", err)
return
}
j.storage.MetricDistributor <- j.makeSeleniumMetric("dns_duration_milliseconds", wr.ri.dnsDuration)
j.storage.MetricDistributor <- j.makeSeleniumMetric("server_connection_duration_milliseconds", wr.ri.serverConnectionDuration)
j.storage.MetricDistributor <- j.makeSeleniumMetric("server_response_duration_milliseconds", wr.ri.serverResponseDuration)
j.storage.MetricDistributor <- j.makeSeleniumMetric("server_processing_duration_milliseconds", wr.ri.serverProcessingDuration)
j.storage.MetricDistributor <- j.makeSeleniumMetric("dom_rendering_duration_milliseconds", wr.ri.domRenderingDuration)
j.storage.MetricDistributor <- j.makeSeleniumMetric("time_to_first_byte_milliseconds", wr.ri.timeToFirstByte)
err = wr.wd.Close()
if err != nil {
log.Println("Error: Could not close Selenium request:", err)
return
}
}
func newWebRequest(url string) webRequest {
rt := &requestTimings{}
ri := &requestIntervals{}
wr := webRequest{
url: url,
rt: rt,
ri: ri,
}
return wr
}
func (wr *webRequest) setRemote(remote string) error {
var err error
caps := selenium.Capabilities(map[string]interface{}{"browserName": "chrome", "cleanSession": true})
wr.wd, err = selenium.NewRemote(caps, remote)
if err != nil {
return fmt.Errorf("failed to open session %v", err)
}
return nil
}
func (wr *webRequest) fetchTiming(obj string) (float64, error) {
ss := fmt.Sprint("return window.performance.timing.", obj)
timing, err := wr.wd.ExecuteScript(ss, nil)
if err != nil {
return 0, fmt.Errorf("Could not fetch timing for %v: %v", obj, err)
}
if timing == nil {
log.Println("Could not fetch timing for", obj)
return 0, nil
}
return timing.(float64), nil
}
func (wr *webRequest) getTimings() error {
var err error
wr.rt.navigationStart, err = wr.fetchTiming("navigationStart")
if err != nil {
return err
}
wr.rt.redirectStart, err = wr.fetchTiming("redirectStart")
if err != nil {
return err
}
wr.rt.redirectEnd, err = wr.fetchTiming("redirectEnd")
if err != nil {
return err
}
wr.rt.fetchStart, err = wr.fetchTiming("fetchStart")
if err != nil {
return err
}
wr.rt.domainLookupStart, err = wr.fetchTiming("domainLookupStart")
if err != nil {
return err
}
wr.rt.domainLookupEnd, err = wr.fetchTiming("domainLookupEnd")
if err != nil {
return err
}
wr.rt.connectStart, err = wr.fetchTiming("connectStart")
if err != nil {
return err
}
wr.rt.connectEnd, err = wr.fetchTiming("connectEnd")
if err != nil {
return err
}
wr.rt.requestStart, err = wr.fetchTiming("requestStart")
if err != nil {
return err
}
wr.rt.responseStart, err = wr.fetchTiming("responseStart")
if err != nil {
return err
}
wr.rt.responseEnd, err = wr.fetchTiming("responseEnd")
if err != nil {
return err
}
wr.rt.domLoading, err = wr.fetchTiming("domLoading")
if err != nil {
return err
}
wr.rt.domInteractive, err = wr.fetchTiming("domInteractive")
if err != nil {
return err
}
wr.rt.domContentLoaded, err = wr.fetchTiming("domContentLoaded")
if err != nil {
return err
}
wr.rt.domComplete, err = wr.fetchTiming("domComplete")
if err != nil {
return err
}
wr.rt.loadEventStart, err = wr.fetchTiming("loadEventStart")
if err != nil {
return err
}
wr.rt.loadEventEnd, err = wr.fetchTiming("loadEventEnd")
if err != nil {
return err
}
wr.calcIntervals()
return nil
}
func (wr *webRequest) calcIntervals() {
// dnsDuration: Time to complete DNS lookup
// domainLookupStart -> domainLookupEnd
wr.ri.dnsDuration = wr.rt.domainLookupEnd - wr.rt.domainLookupStart
// serverConnectionDuration: Time to initiate a TCP connection
// connectStart -> connectEnd
wr.ri.serverConnectionDuration = wr.rt.connectEnd - wr.rt.connectStart
// serverProcessingDuration: Time for the server to process the HTTP request before
// sending first byte
// requestStart -> responseStart
wr.ri.serverProcessingDuration = wr.rt.responseStart - wr.rt.requestStart
// serverResponseDuration: Time for the server to send the entire response
// responseStart -> responseEnd
wr.ri.serverResponseDuration = wr.rt.responseEnd - wr.rt.responseStart
// domRenderingDuration: Time to rendor the complete DOM
// domLoading -> domComplete
wr.ri.domRenderingDuration = wr.rt.domComplete - wr.rt.domLoading
// timeToFirstByte: Time to return the first byte to the client
wr.ri.timeToFirstByte = wr.rt.responseStart - wr.rt.domainLookupStart
}
func (j *SeleniumJob) makeSeleniumMetric(metric string, value float64) Metric {
return makeMetric(metric, value, j.config.Name, j.config.URL, j.config.Tags)
}