1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chromeos/geolocation/simple_geolocation_request.h"
10 #include "base/json/json_reader.h"
11 #include "base/metrics/histogram.h"
12 #include "base/metrics/sparse_histogram.h"
13 #include "base/strings/string_number_conversions.h"
14 #include "base/strings/stringprintf.h"
15 #include "base/time/time.h"
16 #include "base/values.h"
17 #include "chromeos/geolocation/simple_geolocation_provider.h"
18 #include "google_apis/google_api_keys.h"
19 #include "net/base/escape.h"
20 #include "net/base/load_flags.h"
21 #include "net/http/http_status_code.h"
22 #include "net/url_request/url_request_context_getter.h"
23 #include "net/url_request/url_request_status.h"
25 // Location resolve timeout is usually 1 minute, so 2 minutes with 50 buckets
27 #define UMA_HISTOGRAM_LOCATION_RESPONSE_TIMES(name, sample) \
28 UMA_HISTOGRAM_CUSTOM_TIMES(name, \
30 base::TimeDelta::FromMilliseconds(10), \
31 base::TimeDelta::FromMinutes(2), \
38 // The full request text. (no parameters are supported by now)
39 const char kSimpleGeolocationRequestBody
[] = "{\"considerIP\": \"true\"}";
42 const char kLocationString
[] = "location";
43 const char kLatString
[] = "lat";
44 const char kLngString
[] = "lng";
45 const char kAccuracyString
[] = "accuracy";
46 // Error object and its contents.
47 const char kErrorString
[] = "error";
48 // "errors" array in "erorr" object is ignored.
49 const char kCodeString
[] = "code";
50 const char kMessageString
[] = "message";
52 // We are using "sparse" histograms for the number of retry attempts,
53 // so we need to explicitly limit maximum value (in case something goes wrong).
54 const size_t kMaxRetriesValueInHistograms
= 20;
56 // Sleep between geolocation request retry on HTTP error.
57 const unsigned int kResolveGeolocationRetrySleepOnServerErrorSeconds
= 5;
59 // Sleep between geolocation request retry on bad server response.
60 const unsigned int kResolveGeolocationRetrySleepBadResponseSeconds
= 10;
62 enum SimpleGeolocationRequestEvent
{
63 // NOTE: Do not renumber these as that would confuse interpretation of
64 // previously logged data. When making changes, also update the enum list
65 // in tools/metrics/histograms/histograms.xml to keep it in sync.
66 SIMPLE_GEOLOCATION_REQUEST_EVENT_REQUEST_START
= 0,
67 SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_SUCCESS
= 1,
68 SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_NOT_OK
= 2,
69 SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_EMPTY
= 3,
70 SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
= 4,
72 // NOTE: Add entries only immediately above this line.
73 SIMPLE_GEOLOCATION_REQUEST_EVENT_COUNT
= 5
76 enum SimpleGeolocationRequestResult
{
77 // NOTE: Do not renumber these as that would confuse interpretation of
78 // previously logged data. When making changes, also update the enum list
79 // in tools/metrics/histograms/histograms.xml to keep it in sync.
80 SIMPLE_GEOLOCATION_REQUEST_RESULT_SUCCESS
= 0,
81 SIMPLE_GEOLOCATION_REQUEST_RESULT_FAILURE
= 1,
82 SIMPLE_GEOLOCATION_REQUEST_RESULT_SERVER_ERROR
= 2,
83 SIMPLE_GEOLOCATION_REQUEST_RESULT_CANCELLED
= 3,
85 // NOTE: Add entries only immediately above this line.
86 SIMPLE_GEOLOCATION_REQUEST_RESULT_COUNT
= 4
89 // Too many requests (more than 1) mean there is a problem in implementation.
90 void RecordUmaEvent(SimpleGeolocationRequestEvent event
) {
91 UMA_HISTOGRAM_ENUMERATION("SimpleGeolocation.Request.Event",
93 SIMPLE_GEOLOCATION_REQUEST_EVENT_COUNT
);
96 void RecordUmaResponseCode(int code
) {
97 UMA_HISTOGRAM_SPARSE_SLOWLY("SimpleGeolocation.Request.ResponseCode", code
);
100 // Slow geolocation resolve leads to bad user experience.
101 void RecordUmaResponseTime(base::TimeDelta elapsed
, bool success
) {
103 UMA_HISTOGRAM_LOCATION_RESPONSE_TIMES(
104 "SimpleGeolocation.Request.ResponseSuccessTime", elapsed
);
106 UMA_HISTOGRAM_LOCATION_RESPONSE_TIMES(
107 "SimpleGeolocation.Request.ResponseFailureTime", elapsed
);
111 void RecordUmaResult(SimpleGeolocationRequestResult result
, size_t retries
) {
112 UMA_HISTOGRAM_ENUMERATION("SimpleGeolocation.Request.Result",
114 SIMPLE_GEOLOCATION_REQUEST_RESULT_COUNT
);
115 UMA_HISTOGRAM_SPARSE_SLOWLY("SimpleGeolocation.Request.Retries",
116 std::min(retries
, kMaxRetriesValueInHistograms
));
119 // Creates the request url to send to the server.
120 GURL
GeolocationRequestURL(const GURL
& url
) {
121 if (url
!= SimpleGeolocationProvider::DefaultGeolocationProviderURL())
124 std::string api_key
= google_apis::GetAPIKey();
128 std::string
query(url
.query());
131 query
+= "key=" + net::EscapeQueryParamValue(api_key
, true);
132 GURL::Replacements replacements
;
133 replacements
.SetQueryStr(query
);
134 return url
.ReplaceComponents(replacements
);
137 void PrintGeolocationError(const GURL
& server_url
,
138 const std::string
& message
,
139 Geoposition
* position
) {
140 position
->status
= Geoposition::STATUS_SERVER_ERROR
;
141 position
->error_message
=
142 base::StringPrintf("SimpleGeolocation provider at '%s' : %s.",
143 server_url
.GetOrigin().spec().c_str(),
145 VLOG(1) << "SimpleGeolocationRequest::GetGeolocationFromResponse() : "
146 << position
->error_message
;
149 // Parses the server response body. Returns true if parsing was successful.
150 // Sets |*position| to the parsed Geolocation if a valid position was received,
151 // otherwise leaves it unchanged.
152 bool ParseServerResponse(const GURL
& server_url
,
153 const std::string
& response_body
,
154 Geoposition
* position
) {
157 if (response_body
.empty()) {
158 PrintGeolocationError(
159 server_url
, "Server returned empty response", position
);
160 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_EMPTY
);
163 VLOG(1) << "SimpleGeolocationRequest::ParseServerResponse() : "
164 "Parsing response '" << response_body
<< "'";
166 // Parse the response, ignoring comments.
167 std::string error_msg
;
168 scoped_ptr
<base::Value
> response_value(base::JSONReader::ReadAndReturnError(
169 response_body
, base::JSON_PARSE_RFC
, NULL
, &error_msg
));
170 if (response_value
== NULL
) {
171 PrintGeolocationError(
172 server_url
, "JSONReader failed: " + error_msg
, position
);
173 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
);
177 base::DictionaryValue
* response_object
= NULL
;
178 if (!response_value
->GetAsDictionary(&response_object
)) {
179 PrintGeolocationError(
181 "Unexpected response type : " +
182 base::StringPrintf("%u", response_value
->GetType()),
184 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
);
188 base::DictionaryValue
* error_object
= NULL
;
189 base::DictionaryValue
* location_object
= NULL
;
190 response_object
->GetDictionaryWithoutPathExpansion(kLocationString
,
192 response_object
->GetDictionaryWithoutPathExpansion(kErrorString
,
195 position
->timestamp
= base::Time::Now();
198 if (!error_object
->GetStringWithoutPathExpansion(
199 kMessageString
, &(position
->error_message
))) {
200 position
->error_message
= "Server returned error without message.";
203 // Ignore result (code defaults to zero).
204 error_object
->GetIntegerWithoutPathExpansion(kCodeString
,
205 &(position
->error_code
));
207 position
->error_message
.erase();
210 if (location_object
) {
211 if (!location_object
->GetDoubleWithoutPathExpansion(
212 kLatString
, &(position
->latitude
))) {
213 PrintGeolocationError(server_url
, "Missing 'lat' attribute.", position
);
214 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
);
217 if (!location_object
->GetDoubleWithoutPathExpansion(
218 kLngString
, &(position
->longitude
))) {
219 PrintGeolocationError(server_url
, "Missing 'lon' attribute.", position
);
220 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
);
223 if (!response_object
->GetDoubleWithoutPathExpansion(
224 kAccuracyString
, &(position
->accuracy
))) {
225 PrintGeolocationError(
226 server_url
, "Missing 'accuracy' attribute.", position
);
227 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
);
233 position
->status
= Geoposition::STATUS_SERVER_ERROR
;
236 // Empty response is STATUS_OK but not Valid().
237 position
->status
= Geoposition::STATUS_OK
;
241 // Attempts to extract a position from the response. Detects and indicates
242 // various failure cases.
243 bool GetGeolocationFromResponse(bool http_success
,
245 const std::string
& response_body
,
246 const GURL
& server_url
,
247 Geoposition
* position
) {
249 // HttpPost can fail for a number of reasons. Most likely this is because
250 // we're offline, or there was no response.
252 PrintGeolocationError(server_url
, "No response received", position
);
253 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_EMPTY
);
256 if (status_code
!= net::HTTP_OK
) {
257 std::string message
= "Returned error code ";
258 message
+= base::IntToString(status_code
);
259 PrintGeolocationError(server_url
, message
, position
);
260 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_NOT_OK
);
264 return ParseServerResponse(server_url
, response_body
, position
);
269 SimpleGeolocationRequest::SimpleGeolocationRequest(
270 net::URLRequestContextGetter
* url_context_getter
,
271 const GURL
& service_url
,
272 base::TimeDelta timeout
)
273 : url_context_getter_(url_context_getter
),
274 service_url_(service_url
),
275 retry_sleep_on_server_error_(base::TimeDelta::FromSeconds(
276 kResolveGeolocationRetrySleepOnServerErrorSeconds
)),
277 retry_sleep_on_bad_response_(base::TimeDelta::FromSeconds(
278 kResolveGeolocationRetrySleepBadResponseSeconds
)),
283 SimpleGeolocationRequest::~SimpleGeolocationRequest() {
284 DCHECK(thread_checker_
.CalledOnValidThread());
286 // If callback is not empty, request is cancelled.
287 if (!callback_
.is_null()) {
288 RecordUmaResponseTime(base::Time::Now() - request_started_at_
, false);
289 RecordUmaResult(SIMPLE_GEOLOCATION_REQUEST_RESULT_CANCELLED
, retries_
);
293 void SimpleGeolocationRequest::StartRequest() {
294 DCHECK(thread_checker_
.CalledOnValidThread());
295 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_REQUEST_START
);
299 net::URLFetcher::Create(request_url_
, net::URLFetcher::POST
, this));
300 url_fetcher_
->SetRequestContext(url_context_getter_
.get());
301 url_fetcher_
->SetUploadData("application/json",
302 std::string(kSimpleGeolocationRequestBody
));
303 url_fetcher_
->SetLoadFlags(net::LOAD_BYPASS_CACHE
|
304 net::LOAD_DISABLE_CACHE
|
305 net::LOAD_DO_NOT_SAVE_COOKIES
|
306 net::LOAD_DO_NOT_SEND_COOKIES
|
307 net::LOAD_DO_NOT_SEND_AUTH_DATA
);
308 url_fetcher_
->Start();
311 void SimpleGeolocationRequest::MakeRequest(const ResponseCallback
& callback
) {
312 callback_
= callback
;
313 request_url_
= GeolocationRequestURL(service_url_
);
314 timeout_timer_
.Start(
315 FROM_HERE
, timeout_
, this, &SimpleGeolocationRequest::OnTimeout
);
316 request_started_at_
= base::Time::Now();
320 void SimpleGeolocationRequest::Retry(bool server_error
) {
321 base::TimeDelta
delay(server_error
? retry_sleep_on_server_error_
322 : retry_sleep_on_bad_response_
);
323 request_scheduled_
.Start(
324 FROM_HERE
, delay
, this, &SimpleGeolocationRequest::StartRequest
);
327 void SimpleGeolocationRequest::OnURLFetchComplete(
328 const net::URLFetcher
* source
) {
329 DCHECK_EQ(url_fetcher_
.get(), source
);
331 net::URLRequestStatus status
= source
->GetStatus();
332 int response_code
= source
->GetResponseCode();
333 RecordUmaResponseCode(response_code
);
336 source
->GetResponseAsString(&data
);
337 const bool parse_success
= GetGeolocationFromResponse(
338 status
.is_success(), response_code
, data
, source
->GetURL(), &position_
);
339 const bool server_error
=
340 !status
.is_success() || (response_code
>= 500 && response_code
< 600);
341 const bool success
= parse_success
&& position_
.Valid();
342 url_fetcher_
.reset();
344 DVLOG(1) << "SimpleGeolocationRequest::OnURLFetchComplete(): position={"
345 << position_
.ToString() << "}";
351 const base::TimeDelta elapsed
= base::Time::Now() - request_started_at_
;
352 RecordUmaResponseTime(elapsed
, success
);
354 RecordUmaResult(SIMPLE_GEOLOCATION_REQUEST_RESULT_SUCCESS
, retries_
);
356 ReplyAndDestroySelf(elapsed
, server_error
);
357 // "this" is already destroyed here.
360 void SimpleGeolocationRequest::ReplyAndDestroySelf(
361 const base::TimeDelta elapsed
,
363 url_fetcher_
.reset();
364 timeout_timer_
.Stop();
365 request_scheduled_
.Stop();
367 ResponseCallback callback
= callback_
;
369 // Empty callback is used to identify "completed or not yet started request".
372 // callback.Run() usually destroys SimpleGeolocationRequest, because this is
373 // the way callback is implemented in GeolocationProvider.
374 callback
.Run(position_
, server_error
, elapsed
);
375 // "this" is already destroyed here.
378 void SimpleGeolocationRequest::OnTimeout() {
379 const SimpleGeolocationRequestResult result
=
380 (position_
.status
== Geoposition::STATUS_SERVER_ERROR
381 ? SIMPLE_GEOLOCATION_REQUEST_RESULT_SERVER_ERROR
382 : SIMPLE_GEOLOCATION_REQUEST_RESULT_FAILURE
);
383 RecordUmaResult(result
, retries_
);
384 position_
.status
= Geoposition::STATUS_TIMEOUT
;
385 const base::TimeDelta elapsed
= base::Time::Now() - request_started_at_
;
386 ReplyAndDestroySelf(elapsed
, true /* server_error */);
387 // "this" is already destroyed here.
390 } // namespace chromeos