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(
169 base::JSONReader::DeprecatedReadAndReturnError(
170 response_body
, base::JSON_PARSE_RFC
, NULL
, &error_msg
));
171 if (response_value
== NULL
) {
172 PrintGeolocationError(
173 server_url
, "JSONReader failed: " + error_msg
, position
);
174 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
);
178 base::DictionaryValue
* response_object
= NULL
;
179 if (!response_value
->GetAsDictionary(&response_object
)) {
180 PrintGeolocationError(
182 "Unexpected response type : " +
183 base::StringPrintf("%u", response_value
->GetType()),
185 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
);
189 base::DictionaryValue
* error_object
= NULL
;
190 base::DictionaryValue
* location_object
= NULL
;
191 response_object
->GetDictionaryWithoutPathExpansion(kLocationString
,
193 response_object
->GetDictionaryWithoutPathExpansion(kErrorString
,
196 position
->timestamp
= base::Time::Now();
199 if (!error_object
->GetStringWithoutPathExpansion(
200 kMessageString
, &(position
->error_message
))) {
201 position
->error_message
= "Server returned error without message.";
204 // Ignore result (code defaults to zero).
205 error_object
->GetIntegerWithoutPathExpansion(kCodeString
,
206 &(position
->error_code
));
208 position
->error_message
.erase();
211 if (location_object
) {
212 if (!location_object
->GetDoubleWithoutPathExpansion(
213 kLatString
, &(position
->latitude
))) {
214 PrintGeolocationError(server_url
, "Missing 'lat' attribute.", position
);
215 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
);
218 if (!location_object
->GetDoubleWithoutPathExpansion(
219 kLngString
, &(position
->longitude
))) {
220 PrintGeolocationError(server_url
, "Missing 'lon' attribute.", position
);
221 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
);
224 if (!response_object
->GetDoubleWithoutPathExpansion(
225 kAccuracyString
, &(position
->accuracy
))) {
226 PrintGeolocationError(
227 server_url
, "Missing 'accuracy' attribute.", position
);
228 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_MALFORMED
);
234 position
->status
= Geoposition::STATUS_SERVER_ERROR
;
237 // Empty response is STATUS_OK but not Valid().
238 position
->status
= Geoposition::STATUS_OK
;
242 // Attempts to extract a position from the response. Detects and indicates
243 // various failure cases.
244 bool GetGeolocationFromResponse(bool http_success
,
246 const std::string
& response_body
,
247 const GURL
& server_url
,
248 Geoposition
* position
) {
250 // HttpPost can fail for a number of reasons. Most likely this is because
251 // we're offline, or there was no response.
253 PrintGeolocationError(server_url
, "No response received", position
);
254 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_EMPTY
);
257 if (status_code
!= net::HTTP_OK
) {
258 std::string message
= "Returned error code ";
259 message
+= base::IntToString(status_code
);
260 PrintGeolocationError(server_url
, message
, position
);
261 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_RESPONSE_NOT_OK
);
265 return ParseServerResponse(server_url
, response_body
, position
);
270 SimpleGeolocationRequest::SimpleGeolocationRequest(
271 net::URLRequestContextGetter
* url_context_getter
,
272 const GURL
& service_url
,
273 base::TimeDelta timeout
)
274 : url_context_getter_(url_context_getter
),
275 service_url_(service_url
),
276 retry_sleep_on_server_error_(base::TimeDelta::FromSeconds(
277 kResolveGeolocationRetrySleepOnServerErrorSeconds
)),
278 retry_sleep_on_bad_response_(base::TimeDelta::FromSeconds(
279 kResolveGeolocationRetrySleepBadResponseSeconds
)),
284 SimpleGeolocationRequest::~SimpleGeolocationRequest() {
285 DCHECK(thread_checker_
.CalledOnValidThread());
287 // If callback is not empty, request is cancelled.
288 if (!callback_
.is_null()) {
289 RecordUmaResponseTime(base::Time::Now() - request_started_at_
, false);
290 RecordUmaResult(SIMPLE_GEOLOCATION_REQUEST_RESULT_CANCELLED
, retries_
);
294 void SimpleGeolocationRequest::StartRequest() {
295 DCHECK(thread_checker_
.CalledOnValidThread());
296 RecordUmaEvent(SIMPLE_GEOLOCATION_REQUEST_EVENT_REQUEST_START
);
300 net::URLFetcher::Create(request_url_
, net::URLFetcher::POST
, this);
301 url_fetcher_
->SetRequestContext(url_context_getter_
.get());
302 url_fetcher_
->SetUploadData("application/json",
303 std::string(kSimpleGeolocationRequestBody
));
304 url_fetcher_
->SetLoadFlags(net::LOAD_BYPASS_CACHE
|
305 net::LOAD_DISABLE_CACHE
|
306 net::LOAD_DO_NOT_SAVE_COOKIES
|
307 net::LOAD_DO_NOT_SEND_COOKIES
|
308 net::LOAD_DO_NOT_SEND_AUTH_DATA
);
309 url_fetcher_
->Start();
312 void SimpleGeolocationRequest::MakeRequest(const ResponseCallback
& callback
) {
313 callback_
= callback
;
314 request_url_
= GeolocationRequestURL(service_url_
);
315 timeout_timer_
.Start(
316 FROM_HERE
, timeout_
, this, &SimpleGeolocationRequest::OnTimeout
);
317 request_started_at_
= base::Time::Now();
321 void SimpleGeolocationRequest::Retry(bool server_error
) {
322 base::TimeDelta
delay(server_error
? retry_sleep_on_server_error_
323 : retry_sleep_on_bad_response_
);
324 request_scheduled_
.Start(
325 FROM_HERE
, delay
, this, &SimpleGeolocationRequest::StartRequest
);
328 void SimpleGeolocationRequest::OnURLFetchComplete(
329 const net::URLFetcher
* source
) {
330 DCHECK_EQ(url_fetcher_
.get(), source
);
332 net::URLRequestStatus status
= source
->GetStatus();
333 int response_code
= source
->GetResponseCode();
334 RecordUmaResponseCode(response_code
);
337 source
->GetResponseAsString(&data
);
338 const bool parse_success
= GetGeolocationFromResponse(
339 status
.is_success(), response_code
, data
, source
->GetURL(), &position_
);
340 const bool server_error
=
341 !status
.is_success() || (response_code
>= 500 && response_code
< 600);
342 const bool success
= parse_success
&& position_
.Valid();
343 url_fetcher_
.reset();
345 DVLOG(1) << "SimpleGeolocationRequest::OnURLFetchComplete(): position={"
346 << position_
.ToString() << "}";
352 const base::TimeDelta elapsed
= base::Time::Now() - request_started_at_
;
353 RecordUmaResponseTime(elapsed
, success
);
355 RecordUmaResult(SIMPLE_GEOLOCATION_REQUEST_RESULT_SUCCESS
, retries_
);
357 ReplyAndDestroySelf(elapsed
, server_error
);
358 // "this" is already destroyed here.
361 void SimpleGeolocationRequest::ReplyAndDestroySelf(
362 const base::TimeDelta elapsed
,
364 url_fetcher_
.reset();
365 timeout_timer_
.Stop();
366 request_scheduled_
.Stop();
368 ResponseCallback callback
= callback_
;
370 // Empty callback is used to identify "completed or not yet started request".
373 // callback.Run() usually destroys SimpleGeolocationRequest, because this is
374 // the way callback is implemented in GeolocationProvider.
375 callback
.Run(position_
, server_error
, elapsed
);
376 // "this" is already destroyed here.
379 void SimpleGeolocationRequest::OnTimeout() {
380 const SimpleGeolocationRequestResult result
=
381 (position_
.status
== Geoposition::STATUS_SERVER_ERROR
382 ? SIMPLE_GEOLOCATION_REQUEST_RESULT_SERVER_ERROR
383 : SIMPLE_GEOLOCATION_REQUEST_RESULT_FAILURE
);
384 RecordUmaResult(result
, retries_
);
385 position_
.status
= Geoposition::STATUS_TIMEOUT
;
386 const base::TimeDelta elapsed
= base::Time::Now() - request_started_at_
;
387 ReplyAndDestroySelf(elapsed
, true /* server_error */);
388 // "this" is already destroyed here.
391 } // namespace chromeos