| /* |
| * Copyright 2010 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| // Author: abliss@google.com (Adam Bliss) |
| |
| #include "net/instaweb/rewriter/public/add_instrumentation_filter.h" |
| |
| #include "base/logging.h" |
| #include "net/instaweb/http/public/request_context.h" |
| #include "net/instaweb/http/public/request_timing_info.h" |
| #include "net/instaweb/rewriter/public/experiment_util.h" |
| #include "net/instaweb/rewriter/public/request_properties.h" |
| #include "net/instaweb/rewriter/public/rewrite_driver.h" |
| #include "net/instaweb/rewriter/public/rewrite_options.h" |
| #include "net/instaweb/rewriter/public/server_context.h" |
| #include "net/instaweb/rewriter/public/static_asset_manager.h" |
| #include "pagespeed/kernel/base/escaping.h" |
| #include "pagespeed/kernel/base/ref_counted_ptr.h" |
| #include "pagespeed/kernel/base/statistics.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/string_util.h" |
| #include "pagespeed/kernel/html/html_element.h" |
| #include "pagespeed/kernel/html/html_name.h" |
| #include "pagespeed/kernel/html/html_node.h" |
| #include "pagespeed/kernel/http/google_url.h" |
| #include "pagespeed/kernel/http/http_names.h" |
| #include "pagespeed/kernel/http/response_headers.h" |
| |
| namespace net_instaweb { |
| |
| namespace { |
| |
| // The javascript tag to insert in the top of the <head> element. We want this |
| // as early as possible in the html. It must be short and fast. |
| const char kHeadScript[] = |
| "<script type='text/javascript'>" |
| "window.mod_pagespeed_start = Number(new Date());" |
| "</script>"; |
| |
| } // namespace |
| |
| // Timing tag for total page load time. Also embedded in kTailScriptFormat |
| // above via the second %s. |
| // TODO(jud): These values would be better set to "load" and "beforeunload". |
| const char AddInstrumentationFilter::kLoadTag[] = "load:"; |
| const char AddInstrumentationFilter::kUnloadTag[] = "unload:"; |
| |
| // Counters. |
| const char AddInstrumentationFilter::kInstrumentationScriptAddedCount[] = |
| "instrumentation_filter_script_added_count"; |
| AddInstrumentationFilter::AddInstrumentationFilter(RewriteDriver* driver) |
| : CommonFilter(driver), |
| found_head_(false), |
| added_head_script_(false), |
| added_unload_script_(false) { |
| Statistics* stats = driver->server_context()->statistics(); |
| instrumentation_script_added_count_ = stats->GetVariable( |
| kInstrumentationScriptAddedCount); |
| } |
| |
| AddInstrumentationFilter::~AddInstrumentationFilter() {} |
| |
| void AddInstrumentationFilter::InitStats(Statistics* statistics) { |
| statistics->AddVariable(kInstrumentationScriptAddedCount); |
| } |
| |
| void AddInstrumentationFilter::StartDocumentImpl() { |
| found_head_ = false; |
| added_head_script_ = false; |
| added_unload_script_ = false; |
| } |
| |
| void AddInstrumentationFilter::AddHeadScript(HtmlElement* element) { |
| // IE doesn't like tags other than title or meta at the start of the |
| // head. The MSDN page says: |
| // The X-UA-Compatible header isn't case sensitive; however, it must appear |
| // in the header of the webpage (the HEAD section) before all other elements |
| // except for the title element and other meta elements. |
| // Reference: http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx |
| if (element->keyword() != HtmlName::kTitle && |
| element->keyword() != HtmlName::kMeta) { |
| added_head_script_ = true; |
| // TODO(abliss): add an actual element instead, so other filters can |
| // rewrite this JS |
| HtmlCharactersNode* script = driver()->NewCharactersNode(NULL, kHeadScript); |
| driver()->InsertNodeBeforeCurrent(script); |
| instrumentation_script_added_count_->Add(1); |
| } |
| } |
| |
| void AddInstrumentationFilter::StartElementImpl(HtmlElement* element) { |
| if (found_head_ && !added_head_script_) { |
| AddHeadScript(element); |
| } |
| if (!found_head_ && element->keyword() == HtmlName::kHead) { |
| found_head_ = true; |
| } |
| } |
| |
| void AddInstrumentationFilter::EndElementImpl(HtmlElement* element) { |
| if (found_head_ && element->keyword() == HtmlName::kHead) { |
| if (!added_head_script_) { |
| AddHeadScript(element); |
| } |
| if (driver()->options()->report_unload_time() && |
| !added_unload_script_) { |
| GoogleString js = GetScriptJs(kUnloadTag); |
| HtmlElement* script = driver()->NewElement(element, HtmlName::kScript); |
| if (!driver()->defer_instrumentation_script()) { |
| driver()->AddAttribute(script, HtmlName::kDataPagespeedNoDefer, NULL); |
| } |
| driver()->InsertNodeBeforeCurrent(script); |
| AddJsToElement(js, script); |
| added_unload_script_ = true; |
| } |
| } |
| } |
| |
| void AddInstrumentationFilter::EndDocument() { |
| // We relied on the existence of a <head> element. This should have been |
| // assured by add_head_filter. |
| if (!found_head_) { |
| LOG(WARNING) << "No <head> found for URL " << driver()->url(); |
| return; |
| } |
| GoogleString js = GetScriptJs(kLoadTag); |
| HtmlElement* script = driver()->NewElement(NULL, HtmlName::kScript); |
| if (!driver()->defer_instrumentation_script()) { |
| driver()->AddAttribute(script, HtmlName::kDataPagespeedNoDefer, NULL); |
| } |
| InsertNodeAtBodyEnd(script); |
| AddJsToElement(js, script); |
| } |
| |
| GoogleString AddInstrumentationFilter::GetScriptJs(StringPiece event) { |
| GoogleString js; |
| StaticAssetManager* static_asset_manager = |
| driver()->server_context()->static_asset_manager(); |
| // Only add the static JS once. |
| if (!added_unload_script_) { |
| if (driver()->options()->enable_extended_instrumentation()) { |
| js = static_asset_manager->GetAsset( |
| StaticAssetEnum::EXTENDED_INSTRUMENTATION_JS, driver()->options()); |
| } |
| StrAppend(&js, static_asset_manager->GetAsset( |
| StaticAssetEnum::ADD_INSTRUMENTATION_JS, driver()->options())); |
| } |
| |
| GoogleString js_event = (event == kLoadTag) ? "load" : "beforeunload"; |
| |
| const RewriteOptions::BeaconUrl& beacons = driver()->options()->beacon_url(); |
| const GoogleString* beacon_url = |
| driver()->IsHttps() ? &beacons.https : &beacons.http; |
| GoogleString extra_params; |
| if (driver()->options()->running_experiment()) { |
| int experiment_state = driver()->options()->experiment_id(); |
| if (experiment_state != experiment::kExperimentNotSet && |
| experiment_state != experiment::kNoExperiment) { |
| StrAppend(&extra_params, "&exptid=", |
| IntegerToString(driver()->options()->experiment_id())); |
| } |
| } |
| |
| const RequestTimingInfo& timing_info = |
| driver()->request_context()->timing_info(); |
| int64 header_fetch_ms; |
| if (timing_info.GetFetchHeaderLatencyMs(&header_fetch_ms)) { |
| // If time taken to fetch the http header is not set, then the response |
| // came from cache. |
| StrAppend(&extra_params, "&hft=", Integer64ToString(header_fetch_ms)); |
| } |
| int64 fetch_ms; |
| if (timing_info.GetFetchLatencyMs(&fetch_ms)) { |
| // If time taken to fetch the resource is not set, then the response |
| // came from cache. |
| StrAppend(&extra_params, "&ft=", Integer64ToString(fetch_ms)); |
| } |
| int64 ttfb_ms; |
| if (timing_info.GetTimeToFirstByte(&ttfb_ms)) { |
| StrAppend(&extra_params, "&s_ttfb=", Integer64ToString(ttfb_ms)); |
| } |
| |
| // Append the http response code. |
| if (driver()->response_headers() != NULL && |
| driver()->response_headers()->status_code() > 0 && |
| driver()->response_headers()->status_code() != HttpStatus::kOK) { |
| StrAppend(&extra_params, "&rc=", IntegerToString( |
| driver()->response_headers()->status_code())); |
| } |
| // Append the request id. |
| if (driver()->request_context()->request_id() > 0) { |
| StrAppend(&extra_params, "&id=", Integer64ToString( |
| driver()->request_context()->request_id())); |
| } |
| |
| GoogleString html_url; |
| EscapeToJsStringLiteral(driver()->google_url().Spec(), |
| false, /* no quotes */ |
| &html_url); |
| |
| StrAppend(&js, "\npagespeed.addInstrumentationInit("); |
| StrAppend(&js, "'", *beacon_url, "', "); |
| StrAppend(&js, "'", js_event, "', "); |
| StrAppend(&js, "'", extra_params, "', "); |
| StrAppend(&js, "'", html_url, "');"); |
| |
| return js; |
| } |
| |
| void AddInstrumentationFilter::DetermineEnabled(GoogleString* disabled_reason) { |
| set_is_enabled(!driver()->request_properties()->IsBot()); |
| } |
| |
| } // namespace net_instaweb |