• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2012 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/**
32 * @constructor
33 * @extends {WebInspector.Object}
34 * @implements {WebInspector.ContentProvider}
35 * @param {!NetworkAgent.RequestId} requestId
36 * @param {string} url
37 * @param {string} documentURL
38 * @param {!PageAgent.FrameId} frameId
39 * @param {!NetworkAgent.LoaderId} loaderId
40 */
41WebInspector.NetworkRequest = function(requestId, url, documentURL, frameId, loaderId)
42{
43    this._requestId = requestId;
44    this.url = url;
45    this._documentURL = documentURL;
46    this._frameId = frameId;
47    this._loaderId = loaderId;
48    this._startTime = -1;
49    this._endTime = -1;
50
51    this.statusCode = 0;
52    this.statusText = "";
53    this.requestMethod = "";
54    this.requestTime = 0;
55
56    this._type = WebInspector.resourceTypes.Other;
57    this._contentEncoded = false;
58    this._pendingContentCallbacks = [];
59    this._frames = [];
60
61    this._responseHeaderValues = {};
62}
63
64WebInspector.NetworkRequest.Events = {
65    FinishedLoading: "FinishedLoading",
66    TimingChanged: "TimingChanged",
67    RequestHeadersChanged: "RequestHeadersChanged",
68    ResponseHeadersChanged: "ResponseHeadersChanged",
69}
70
71/** @enum {string} */
72WebInspector.NetworkRequest.InitiatorType = {
73    Other: "other",
74    Parser: "parser",
75    Redirect: "redirect",
76    Script: "script"
77}
78
79/** @typedef {!{name: string, value: string}} */
80WebInspector.NetworkRequest.NameValue;
81
82WebInspector.NetworkRequest.prototype = {
83    /**
84     * @return {!NetworkAgent.RequestId}
85     */
86    get requestId()
87    {
88        return this._requestId;
89    },
90
91    set requestId(requestId)
92    {
93        this._requestId = requestId;
94    },
95
96    /**
97     * @return {string}
98     */
99    get url()
100    {
101        return this._url;
102    },
103
104    set url(x)
105    {
106        if (this._url === x)
107            return;
108
109        this._url = x;
110        this._parsedURL = new WebInspector.ParsedURL(x);
111        delete this._queryString;
112        delete this._parsedQueryParameters;
113        delete this._name;
114        delete this._path;
115    },
116
117    /**
118     * @return {string}
119     */
120    get documentURL()
121    {
122        return this._documentURL;
123    },
124
125    get parsedURL()
126    {
127        return this._parsedURL;
128    },
129
130    /**
131     * @return {!PageAgent.FrameId}
132     */
133    get frameId()
134    {
135        return this._frameId;
136    },
137
138    /**
139     * @return {!NetworkAgent.LoaderId}
140     */
141    get loaderId()
142    {
143        return this._loaderId;
144    },
145
146    /**
147     * @return {number}
148     */
149    get startTime()
150    {
151        return this._startTime || -1;
152    },
153
154    set startTime(x)
155    {
156        this._startTime = x;
157    },
158
159    /**
160     * @return {number}
161     */
162    get responseReceivedTime()
163    {
164        return this._responseReceivedTime || -1;
165    },
166
167    set responseReceivedTime(x)
168    {
169        this._responseReceivedTime = x;
170    },
171
172    /**
173     * @return {number}
174     */
175    get endTime()
176    {
177        return this._endTime || -1;
178    },
179
180    set endTime(x)
181    {
182        if (this.timing && this.timing.requestTime) {
183            // Check against accurate responseReceivedTime.
184            this._endTime = Math.max(x, this.responseReceivedTime);
185        } else {
186            // Prefer endTime since it might be from the network stack.
187            this._endTime = x;
188            if (this._responseReceivedTime > x)
189                this._responseReceivedTime = x;
190        }
191    },
192
193    /**
194     * @return {number}
195     */
196    get duration()
197    {
198        if (this._endTime === -1 || this._startTime === -1)
199            return -1;
200        return this._endTime - this._startTime;
201    },
202
203    /**
204     * @return {number}
205     */
206    get latency()
207    {
208        if (this._responseReceivedTime === -1 || this._startTime === -1)
209            return -1;
210        return this._responseReceivedTime - this._startTime;
211    },
212
213    /**
214     * @return {number}
215     */
216    get resourceSize()
217    {
218        return this._resourceSize || 0;
219    },
220
221    set resourceSize(x)
222    {
223        this._resourceSize = x;
224    },
225
226    /**
227     * @return {number}
228     */
229    get transferSize()
230    {
231        if (typeof this._transferSize === "number")
232            return this._transferSize;
233        if (this.statusCode === 304) // Not modified
234            return this.responseHeadersSize;
235        if (this._cached)
236            return 0;
237        // If we did not receive actual transfer size from network
238        // stack, we prefer using Content-Length over resourceSize as
239        // resourceSize may differ from actual transfer size if platform's
240        // network stack performed decoding (e.g. gzip decompression).
241        // The Content-Length, though, is expected to come from raw
242        // response headers and will reflect actual transfer length.
243        // This won't work for chunked content encoding, so fall back to
244        // resourceSize when we don't have Content-Length. This still won't
245        // work for chunks with non-trivial encodings. We need a way to
246        // get actual transfer size from the network stack.
247        var bodySize = Number(this.responseHeaderValue("Content-Length") || this.resourceSize);
248        return this.responseHeadersSize + bodySize;
249    },
250
251    /**
252     * @param {number} x
253     */
254    increaseTransferSize: function(x)
255    {
256        this._transferSize = (this._transferSize || 0) + x;
257    },
258
259    /**
260     * @return {boolean}
261     */
262    get finished()
263    {
264        return this._finished;
265    },
266
267    set finished(x)
268    {
269        if (this._finished === x)
270            return;
271
272        this._finished = x;
273
274        if (x) {
275            this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.FinishedLoading, this);
276            if (this._pendingContentCallbacks.length)
277                this._innerRequestContent();
278        }
279    },
280
281    /**
282     * @return {boolean}
283     */
284    get failed()
285    {
286        return this._failed;
287    },
288
289    set failed(x)
290    {
291        this._failed = x;
292    },
293
294    /**
295     * @return {boolean}
296     */
297    get canceled()
298    {
299        return this._canceled;
300    },
301
302    set canceled(x)
303    {
304        this._canceled = x;
305    },
306
307    /**
308     * @return {boolean}
309     */
310    get cached()
311    {
312        return !!this._cached && !this._transferSize;
313    },
314
315    set cached(x)
316    {
317        this._cached = x;
318        if (x)
319            delete this._timing;
320    },
321
322    /**
323     * @return {!NetworkAgent.ResourceTiming|undefined}
324     */
325    get timing()
326    {
327        return this._timing;
328    },
329
330    set timing(x)
331    {
332        if (x && !this._cached) {
333            // Take startTime and responseReceivedTime from timing data for better accuracy.
334            // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis.
335            this._startTime = x.requestTime;
336            this._responseReceivedTime = x.requestTime + x.receiveHeadersEnd / 1000.0;
337
338            this._timing = x;
339            this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.TimingChanged, this);
340        }
341    },
342
343    /**
344     * @return {string}
345     */
346    get mimeType()
347    {
348        return this._mimeType;
349    },
350
351    set mimeType(x)
352    {
353        this._mimeType = x;
354    },
355
356    /**
357     * @return {string}
358     */
359    get displayName()
360    {
361        return this._parsedURL.displayName;
362    },
363
364    name: function()
365    {
366        if (this._name)
367            return this._name;
368        this._parseNameAndPathFromURL();
369        return this._name;
370    },
371
372    path: function()
373    {
374        if (this._path)
375            return this._path;
376        this._parseNameAndPathFromURL();
377        return this._path;
378    },
379
380    _parseNameAndPathFromURL: function()
381    {
382        if (this._parsedURL.isDataURL()) {
383            this._name = this._parsedURL.dataURLDisplayName();
384            this._path = "";
385        } else if (this._parsedURL.isAboutBlank()) {
386            this._name = this._parsedURL.url;
387            this._path = "";
388        } else {
389            this._path = this._parsedURL.host + this._parsedURL.folderPathComponents;
390            this._path = this._path.trimURL(WebInspector.inspectedPageDomain ? WebInspector.inspectedPageDomain : "");
391            if (this._parsedURL.lastPathComponent || this._parsedURL.queryParams)
392                this._name = this._parsedURL.lastPathComponent + (this._parsedURL.queryParams ? "?" + this._parsedURL.queryParams : "");
393            else if (this._parsedURL.folderPathComponents) {
394                this._name = this._parsedURL.folderPathComponents.substring(this._parsedURL.folderPathComponents.lastIndexOf("/") + 1) + "/";
395                this._path = this._path.substring(0, this._path.lastIndexOf("/"));
396            } else {
397                this._name = this._parsedURL.host;
398                this._path = "";
399            }
400        }
401    },
402
403    /**
404     * @return {string}
405     */
406    get folder()
407    {
408        var path = this._parsedURL.path;
409        var indexOfQuery = path.indexOf("?");
410        if (indexOfQuery !== -1)
411            path = path.substring(0, indexOfQuery);
412        var lastSlashIndex = path.lastIndexOf("/");
413        return lastSlashIndex !== -1 ? path.substring(0, lastSlashIndex) : "";
414    },
415
416    /**
417     * @return {!WebInspector.ResourceType}
418     */
419    get type()
420    {
421        return this._type;
422    },
423
424    set type(x)
425    {
426        this._type = x;
427    },
428
429    /**
430     * @return {string}
431     */
432    get domain()
433    {
434        return this._parsedURL.host;
435    },
436
437    /**
438     * @return {string}
439     */
440    get scheme()
441    {
442        return this._parsedURL.scheme;
443    },
444
445    /**
446     * @return {?WebInspector.NetworkRequest}
447     */
448    get redirectSource()
449    {
450        if (this.redirects && this.redirects.length > 0)
451            return this.redirects[this.redirects.length - 1];
452        return this._redirectSource;
453    },
454
455    set redirectSource(x)
456    {
457        this._redirectSource = x;
458        delete this._initiatorInfo;
459    },
460
461    /**
462     * @return {!Array.<!WebInspector.NetworkRequest.NameValue>}
463     */
464    requestHeaders: function()
465    {
466        return this._requestHeaders || [];
467    },
468
469    /**
470     * @param {!Array.<!WebInspector.NetworkRequest.NameValue>} headers
471     */
472    setRequestHeaders: function(headers)
473    {
474        this._requestHeaders = headers;
475        delete this._requestCookies;
476
477        this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.RequestHeadersChanged);
478    },
479
480    /**
481     * @return {string|undefined}
482     */
483    requestHeadersText: function()
484    {
485        return this._requestHeadersText;
486    },
487
488    /**
489     * @param {string} text
490     */
491    setRequestHeadersText: function(text)
492    {
493        this._requestHeadersText = text;
494
495        this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.RequestHeadersChanged);
496    },
497
498    /**
499     * @param {string} headerName
500     * @return {string|undefined}
501     */
502    requestHeaderValue: function(headerName)
503    {
504        return this._headerValue(this.requestHeaders(), headerName);
505    },
506
507    /**
508     * @return {!Array.<!WebInspector.Cookie>}
509     */
510    get requestCookies()
511    {
512        if (!this._requestCookies)
513            this._requestCookies = WebInspector.CookieParser.parseCookie(this.requestHeaderValue("Cookie"));
514        return this._requestCookies;
515    },
516
517    /**
518     * @return {string|undefined}
519     */
520    get requestFormData()
521    {
522        return this._requestFormData;
523    },
524
525    set requestFormData(x)
526    {
527        this._requestFormData = x;
528        delete this._parsedFormParameters;
529    },
530
531    /**
532     * @return {string|undefined}
533     */
534    requestHttpVersion: function()
535    {
536        var headersText = this.requestHeadersText();
537        if (!headersText)
538            return undefined;
539        var firstLine = headersText.split(/\r\n/)[0];
540        var match = firstLine.match(/(HTTP\/\d+\.\d+)$/);
541        return match ? match[1] : undefined;
542    },
543
544    /**
545     * @return {!Array.<!WebInspector.NetworkRequest.NameValue>}
546     */
547    get responseHeaders()
548    {
549        return this._responseHeaders || [];
550    },
551
552    set responseHeaders(x)
553    {
554        this._responseHeaders = x;
555        delete this._sortedResponseHeaders;
556        delete this._responseCookies;
557        this._responseHeaderValues = {};
558
559        this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.ResponseHeadersChanged);
560    },
561
562    /**
563     * @return {string}
564     */
565    get responseHeadersText()
566    {
567        if (typeof this._responseHeadersText === "undefined") {
568            this._responseHeadersText = "HTTP/1.1 " + this.statusCode + " " + this.statusText + "\r\n";
569            for (var i = 0; i < this.responseHeaders.length; ++i)
570                this._responseHeadersText += this.responseHeaders[i].name + ": " + this.responseHeaders[i].value + "\r\n";
571        }
572        return this._responseHeadersText;
573    },
574
575    set responseHeadersText(x)
576    {
577        this._responseHeadersText = x;
578
579        this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.ResponseHeadersChanged);
580    },
581
582    /**
583     * @return {number}
584     */
585    get responseHeadersSize()
586    {
587        return this.responseHeadersText.length;
588    },
589
590    /**
591     * @return {!Array.<!WebInspector.NetworkRequest.NameValue>}
592     */
593    get sortedResponseHeaders()
594    {
595        if (this._sortedResponseHeaders !== undefined)
596            return this._sortedResponseHeaders;
597
598        this._sortedResponseHeaders = this.responseHeaders.slice();
599        this._sortedResponseHeaders.sort(function(a, b) { return a.name.toLowerCase().compareTo(b.name.toLowerCase()); });
600        return this._sortedResponseHeaders;
601    },
602
603    /**
604     * @param {string} headerName
605     * @return {string|undefined}
606     */
607    responseHeaderValue: function(headerName)
608    {
609        var value = this._responseHeaderValues[headerName];
610        if (value === undefined) {
611            value = this._headerValue(this.responseHeaders, headerName);
612            this._responseHeaderValues[headerName] = (value !== undefined) ? value : null;
613        }
614        return (value !== null) ? value : undefined;
615    },
616
617    /**
618     * @return {!Array.<!WebInspector.Cookie>}
619     */
620    get responseCookies()
621    {
622        if (!this._responseCookies)
623            this._responseCookies = WebInspector.CookieParser.parseSetCookie(this.responseHeaderValue("Set-Cookie"));
624        return this._responseCookies;
625    },
626
627    /**
628     * @return {?string}
629     */
630    queryString: function()
631    {
632        if (this._queryString !== undefined)
633            return this._queryString;
634
635        var queryString = null;
636        var url = this.url;
637        var questionMarkPosition = url.indexOf("?");
638        if (questionMarkPosition !== -1) {
639            queryString = url.substring(questionMarkPosition + 1);
640            var hashSignPosition = queryString.indexOf("#");
641            if (hashSignPosition !== -1)
642                queryString = queryString.substring(0, hashSignPosition);
643        }
644        this._queryString = queryString;
645        return this._queryString;
646    },
647
648    /**
649     * @return {?Array.<!WebInspector.NetworkRequest.NameValue>}
650     */
651    get queryParameters()
652    {
653        if (this._parsedQueryParameters)
654            return this._parsedQueryParameters;
655        var queryString = this.queryString();
656        if (!queryString)
657            return null;
658        this._parsedQueryParameters = this._parseParameters(queryString);
659        return this._parsedQueryParameters;
660    },
661
662    /**
663     * @return {?Array.<!WebInspector.NetworkRequest.NameValue>}
664     */
665    get formParameters()
666    {
667        if (this._parsedFormParameters)
668            return this._parsedFormParameters;
669        if (!this.requestFormData)
670            return null;
671        var requestContentType = this.requestContentType();
672        if (!requestContentType || !requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
673            return null;
674        this._parsedFormParameters = this._parseParameters(this.requestFormData);
675        return this._parsedFormParameters;
676    },
677
678    /**
679     * @return {string|undefined}
680     */
681    get responseHttpVersion()
682    {
683        var match = this.responseHeadersText.match(/^(HTTP\/\d+\.\d+)/);
684        return match ? match[1] : undefined;
685    },
686
687    /**
688     * @param {string} queryString
689     * @return {!Array.<!WebInspector.NetworkRequest.NameValue>}
690     */
691    _parseParameters: function(queryString)
692    {
693        function parseNameValue(pair)
694        {
695            var splitPair = pair.split("=", 2);
696            return {name: splitPair[0], value: splitPair[1] || ""};
697        }
698        return queryString.split("&").map(parseNameValue);
699    },
700
701    /**
702     * @param {!Array.<!WebInspector.NetworkRequest.NameValue>} headers
703     * @param {string} headerName
704     * @return {string|undefined}
705     */
706    _headerValue: function(headers, headerName)
707    {
708        headerName = headerName.toLowerCase();
709
710        var values = [];
711        for (var i = 0; i < headers.length; ++i) {
712            if (headers[i].name.toLowerCase() === headerName)
713                values.push(headers[i].value);
714        }
715        if (!values.length)
716            return undefined;
717        // Set-Cookie values should be separated by '\n', not comma, otherwise cookies could not be parsed.
718        if (headerName === "set-cookie")
719            return values.join("\n");
720        return values.join(", ");
721    },
722
723    /**
724     * @return {?string|undefined}
725     */
726    get content()
727    {
728        return this._content;
729    },
730
731    /**
732     * @return {boolean}
733     */
734    get contentEncoded()
735    {
736        return this._contentEncoded;
737    },
738
739    /**
740     * @return {string}
741     */
742    contentURL: function()
743    {
744        return this._url;
745    },
746
747    /**
748     * @return {!WebInspector.ResourceType}
749     */
750    contentType: function()
751    {
752        return this._type;
753    },
754
755    /**
756     * @param {function(?string)} callback
757     */
758    requestContent: function(callback)
759    {
760        // We do not support content retrieval for WebSockets at the moment.
761        // Since WebSockets are potentially long-living, fail requests immediately
762        // to prevent caller blocking until resource is marked as finished.
763        if (this.type === WebInspector.resourceTypes.WebSocket) {
764            callback(null);
765            return;
766        }
767        if (typeof this._content !== "undefined") {
768            callback(this.content || null);
769            return;
770        }
771        this._pendingContentCallbacks.push(callback);
772        if (this.finished)
773            this._innerRequestContent();
774    },
775
776    /**
777     * @param {string} query
778     * @param {boolean} caseSensitive
779     * @param {boolean} isRegex
780     * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
781     */
782    searchInContent: function(query, caseSensitive, isRegex, callback)
783    {
784        callback([]);
785    },
786
787    /**
788     * @return {boolean}
789     */
790    isHttpFamily: function()
791    {
792        return !!this.url.match(/^https?:/i);
793    },
794
795    /**
796     * @return {string|undefined}
797     */
798    requestContentType: function()
799    {
800        return this.requestHeaderValue("Content-Type");
801    },
802
803    /**
804     * @return {boolean}
805     */
806    isPingRequest: function()
807    {
808        return "text/ping" === this.requestContentType();
809    },
810
811    /**
812     * @return {boolean}
813     */
814    hasErrorStatusCode: function()
815    {
816        return this.statusCode >= 400;
817    },
818
819    /**
820     * @param {!Element} image
821     */
822    populateImageSource: function(image)
823    {
824        /**
825         * @this {WebInspector.NetworkRequest}
826         * @param {?string} content
827         */
828        function onResourceContent(content)
829        {
830            var imageSrc = this.asDataURL();
831            if (imageSrc === null)
832                imageSrc = this.url;
833            image.src = imageSrc;
834        }
835
836        this.requestContent(onResourceContent.bind(this));
837    },
838
839    /**
840     * @return {?string}
841     */
842    asDataURL: function()
843    {
844        return WebInspector.contentAsDataURL(this._content, this.mimeType, this._contentEncoded);
845    },
846
847    _innerRequestContent: function()
848    {
849        if (this._contentRequested)
850            return;
851        this._contentRequested = true;
852
853        /**
854         * @param {?Protocol.Error} error
855         * @param {string} content
856         * @param {boolean} contentEncoded
857         * @this {WebInspector.NetworkRequest}
858         */
859        function onResourceContent(error, content, contentEncoded)
860        {
861            this._content = error ? null : content;
862            this._contentEncoded = contentEncoded;
863            var callbacks = this._pendingContentCallbacks.slice();
864            for (var i = 0; i < callbacks.length; ++i)
865                callbacks[i](this._content);
866            this._pendingContentCallbacks.length = 0;
867            delete this._contentRequested;
868        }
869        NetworkAgent.getResponseBody(this._requestId, onResourceContent.bind(this));
870    },
871
872    /**
873     * @return {!{type: !WebInspector.NetworkRequest.InitiatorType, url: string, source: string, lineNumber: number, columnNumber: number}}
874     */
875    initiatorInfo: function()
876    {
877        if (this._initiatorInfo)
878            return this._initiatorInfo;
879
880        var type = WebInspector.NetworkRequest.InitiatorType.Other;
881        var url = "";
882        var lineNumber = -Infinity;
883        var columnNumber = -Infinity;
884
885        if (this.redirectSource) {
886            type = WebInspector.NetworkRequest.InitiatorType.Redirect;
887            url = this.redirectSource.url;
888        } else if (this.initiator) {
889            if (this.initiator.type === NetworkAgent.InitiatorType.Parser) {
890                type = WebInspector.NetworkRequest.InitiatorType.Parser;
891                url = this.initiator.url;
892                lineNumber = this.initiator.lineNumber;
893            } else if (this.initiator.type === NetworkAgent.InitiatorType.Script) {
894                var topFrame = this.initiator.stackTrace[0];
895                if (topFrame.url) {
896                    type = WebInspector.NetworkRequest.InitiatorType.Script;
897                    url = topFrame.url;
898                    lineNumber = topFrame.lineNumber;
899                    columnNumber = topFrame.columnNumber;
900                }
901            }
902        }
903
904        this._initiatorInfo = {type: type, url: url, source: WebInspector.displayNameForURL(url), lineNumber: lineNumber, columnNumber: columnNumber};
905        return this._initiatorInfo;
906    },
907
908    /**
909     * @return {!Array.<!Object>}
910     */
911    frames: function()
912    {
913        return this._frames;
914    },
915
916    /**
917     * @param {number} position
918     * @return {!Object|undefined}
919     */
920    frame: function(position)
921    {
922        return this._frames[position];
923    },
924
925    /**
926     * @param {string} errorMessage
927     * @param {number} time
928     */
929    addFrameError: function(errorMessage, time)
930    {
931        this._pushFrame({errorMessage: errorMessage, time: time});
932    },
933
934    /**
935     * @param {!NetworkAgent.WebSocketFrame} response
936     * @param {number} time
937     * @param {boolean} sent
938     */
939    addFrame: function(response, time, sent)
940    {
941        response.time = time;
942        if (sent)
943            response.sent = sent;
944        this._pushFrame(response);
945    },
946
947    /**
948     * @param {!Object} frameOrError
949     */
950    _pushFrame: function(frameOrError)
951    {
952        if (this._frames.length >= 100)
953            this._frames.splice(0, 10);
954        this._frames.push(frameOrError);
955    },
956
957    __proto__: WebInspector.Object.prototype
958}
959