blob: 7a597b33b9b3c6cfb58352d6544c637f13dc0e03 [file] [log] [blame]
/*
* Copyright 2011 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: nforman@google.com (Naomi Forman)
//
// This provides the InsertGAFilter class which adds a Google Analytics
// snippet to html pages.
#ifndef NET_INSTAWEB_REWRITER_PUBLIC_INSERT_GA_FILTER_H_
#define NET_INSTAWEB_REWRITER_PUBLIC_INSERT_GA_FILTER_H_
#include "net/instaweb/rewriter/public/common_filter.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "pagespeed/kernel/base/basictypes.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_node.h"
namespace net_instaweb {
class Statistics;
class Variable;
// Visible only for use in tests.
extern const char kGAExperimentSnippet[];
extern const char kGAJsSnippet[];
extern const char kAnalyticsJsSnippet[];
extern const char kAnalyticsJsIncreaseSiteSpeedTracking[];
extern const char kAnalyticsJsIncreaseSiteSpeedTrackingMinimal[];
extern const char kContentExperimentsJsClientUrl[];
extern const char kContentExperimentsNonNumericVariantComment[];
extern const char kContentExperimentsSetChosenVariationSnippet[];
extern const char kContentExperimentsSetExpAndVariantSnippet[];
extern const char kGASpeedTracking[];
// This class is the implementation of the insert_ga filter, which handles:
// * Adding a Google Analytics snippet to html pages.
// * Adding js to report experiment data to Google Analytics.
class InsertGAFilter : public CommonFilter {
public:
explicit InsertGAFilter(RewriteDriver* rewrite_driver);
virtual ~InsertGAFilter();
// Set up statistics for this filter.
static void InitStats(Statistics* stats);
virtual void StartDocumentImpl();
virtual void StartElementImpl(HtmlElement* element);
virtual void EndElementImpl(HtmlElement* element);
// HTML Events we expect to be in <script> elements.
virtual void Characters(HtmlCharactersNode* characters);
virtual const char* Name() const { return "InsertGASnippet"; }
private:
enum AnalyticsStatus {
kGaJs, // Traditional ga.js or urchin.js.
kAnalyticsJs, // New "universal analytics" analytics.js.
kNoSnippetFound, // Didn't find either.
kUnusableSnippetFound, // There's a snippet on the page, but it's unusual
// and we can't work with it.
};
// For RewriteInlineScript's state machine.
enum ParseState {
kInitial,
kGotGa,
kGotGaFuncCall,
kGotGaCreate,
kGotGaSend,
kGotGaCreateComma,
kGotFieldsObject,
kGotGaSendComma,
kGotGaSendPageview,
kSuccess
};
// Construct the custom variable part for experiment of the GA snippet.
GoogleString ConstructExperimentSnippet() const;
// If appropriate, insert the GA snippet at the end of the document.
virtual void EndDocument();
// If RewriteInlineScript left work to do, finish it now.
void HandleEndScript(HtmlElement* script);
// Handle the body of a script tag.
// * Look for a GA snippet in the script and record the findings so that we
// can optionally add the analytics js at the end of the body if no GA
// snippet is present on the page.
// * If a snippet is present, modify it to add experiment tracking.
void RewriteInlineScript(HtmlCharactersNode* characters);
// Indicates whether or not the string contains a GA snippet with the
// same id as ga_id_, and if so whether it's new-style or old-style.
//
// Expects to be called on every script in the document. Non-const because it
// needs to use seen_sync_ga_js_ to hold state: whether something is a ga.js
// snippet depends in part on whether we've already seen a ga.js library load.
AnalyticsStatus FindSnippetInScript(const GoogleString& s);
// Determine the snippet of JS we need to log a content experiment.
GoogleString AnalyticsJsExperimentSnippet() const;
GoogleString GaJsExperimentSnippet() const;
// Note: logs a warning if we're running with analytics.js and have asked it
// to log to a custom variable (which isn't possible).
bool ShouldInsertExperimentTracking(bool analytics_js);
bool StringLiteralMatches(StringPiece literal, StringPiece desired);
bool StringLiteralEndsWith(StringPiece literal, StringPiece desired);
// Stats on how many tags we moved.
Variable* inserted_ga_snippets_count_;
// Script element we're currently in, so we can check it to see if
// it has the GA snippet already.
HtmlElement* script_element_;
// Whether we added the analytics js or not.
bool added_analytics_js_;
// Whether we added the experiment snippet or not.
bool added_experiment_snippet_;
// GA ID for this site.
GoogleString ga_id_;
// Indicates whether or not we've already found a GA snippet so we know
// whether we need to insert one.
bool found_snippet_;
// Increase site-speed tracking to the max allowed.
bool increase_speed_tracking_;
// The synchronous usage of ga.js is split over two tags: one to load the
// library then one to use it. This is set to true if we've seen something
// that might be the library load.
bool seen_sync_ga_js_;
// RewriteInlineScript runs to process the body of the GA JS inline script.
// Sometimes it needs to save text for later to be added as a new script body
// when it gets the end element event for the script.
GoogleString postponed_script_body_;
DISALLOW_COPY_AND_ASSIGN(InsertGAFilter);
};
} // namespace net_instaweb
#endif // NET_INSTAWEB_REWRITER_PUBLIC_INSERT_GA_FILTER_H_