blob: 62b812b84470b1b7bc08125ba3f51da90fa467bd [file] [log] [blame]
/*
* Copyright 2012 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)
//
// Functionality for manipulating exeriment state and cookies.
#include "net/instaweb/rewriter/public/experiment_util.h"
#include <cstdlib>
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "pagespeed/kernel/base/time_util.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/http/user_agent_matcher.h"
namespace net_instaweb {
namespace experiment {
bool GetExperimentCookieState(const RequestHeaders& headers, int* value) {
ConstStringStarVector v;
*value = kExperimentNotSet;
if (headers.Lookup(HttpAttributes::kCookie, &v)) {
for (int i = 0, nv = v.size(); i < nv; ++i) {
StringPieceVector cookies;
SplitStringPieceToVector(*(v[i]), ";", &cookies, true);
for (int j = 0, ncookies = cookies.size(); j < ncookies; ++j) {
StringPiece cookie(cookies[j]);
TrimWhitespace(&cookie);
if (StringCaseStartsWith(cookie, kExperimentCookiePrefix)) {
cookie.remove_prefix(STATIC_STRLEN(kExperimentCookiePrefix));
*value = CookieStringToState(cookie);
// If we got a bogus value for the cookie, keep looking for another
// one just in case.
if (*value != kExperimentNotSet) {
return true;
}
}
}
}
}
return false;
}
void RemoveExperimentCookie(RequestHeaders* headers) {
headers->RemoveCookie(kExperimentCookie);
}
void SetExperimentCookie(ResponseHeaders* headers,
int state,
const StringPiece& url,
int64 expiration_time_ms) {
GoogleUrl request_url(url);
// If we can't parse this url, don't try to set headers on the response.
if (!request_url.IsWebValid()) {
return;
}
GoogleString expires;
ConvertTimeToString(expiration_time_ms, &expires);
StringPiece host = request_url.Host();
if (host.length() == 0) {
return;
}
GoogleString value = StringPrintf(
"%s=%s; Expires=%s; Domain=.%s; Path=/",
kExperimentCookie, ExperimentStateToCookieString(state).c_str(),
expires.c_str(), host.as_string().c_str());
headers->Add(HttpAttributes::kSetCookie, value);
headers->ComputeCaching();
}
// TODO(nforman): Is this a reasonable way of getting the appropriate
// percentage of the traffic?
// It might be "safer" to do this as a hash of ip so that if one person
// sent simultaneous requests, they would end up on the same side of the
// experiment for all requests.
int DetermineExperimentState(const RewriteOptions* options,
const RequestHeaders& request_headers,
const UserAgentMatcher& agent_matcher) {
int ret = kExperimentNotSet;
int num_experiments = options->num_experiments();
// If are no experiments, return kExperimentNotSet so RewriteOptions doesn't
// try to change.
if (num_experiments < 1) {
return ret;
}
const char* user_agent = request_headers.Lookup1(HttpAttributes::kUserAgent);
UserAgentMatcher::DeviceType device_type =
agent_matcher.GetDeviceTypeForUA(user_agent);
int64 bound = 0;
int64 index = random();
ret = kNoExperiment;
// One of these should be the control.
for (int i = 0; i < num_experiments; ++i) {
RewriteOptions::ExperimentSpec* spec = options->experiment_spec(i);
double mult = static_cast<double>(spec->percent())/100.0;
// Because RewriteOptions checks to make sure the total experiment
// percentage is not greater than 100, bound should never be greater
// than RAND_MAX.
bound += (mult * RAND_MAX);
if (index < bound) {
// At this point we have determined the bucket for this request,
// however that bucket may have a device type match condition.
// In the case where the device type does not match, we still want
// to break out of the loop, otherwise we will "overflow" into the
// next bucket and mess up all the bucket size percentages.
if (spec->matches_device_type(device_type)) {
ret = spec->id();
}
return ret;
}
}
return ret;
}
bool AnyActiveExperiments(const RewriteOptions* options) {
for (int i = 0, n = options->num_experiments(); i < n ; ++i) {
if (options->experiment_spec(i)->percent() > 0) {
return true;
}
}
return false;
}
int CookieStringToState(const StringPiece& cookie_str) {
int ret;
if (!StringToInt(cookie_str, &ret)) {
ret = kExperimentNotSet;
}
return ret;
}
GoogleString ExperimentStateToCookieString(int state) {
GoogleString cookie_value = IntegerToString(state);
return cookie_value;
}
} // namespace experiment
} // namespace net_instaweb