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/timezone/timezone_request.h"
9 #include "base/json/json_reader.h"
10 #include "base/metrics/histogram.h"
11 #include "base/metrics/sparse_histogram.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/stringprintf.h"
14 #include "base/time/time.h"
15 #include "base/values.h"
16 #include "chromeos/geolocation/geoposition.h"
17 #include "google_apis/google_api_keys.h"
18 #include "net/base/escape.h"
19 #include "net/base/load_flags.h"
20 #include "net/http/http_status_code.h"
21 #include "net/url_request/url_fetcher.h"
22 #include "net/url_request/url_request_context_getter.h"
23 #include "net/url_request/url_request_status.h"
29 const char kDefaultTimezoneProviderUrl
[] =
30 "https://maps.googleapis.com/maps/api/timezone/json?";
32 const char kKeyString
[] = "key";
33 // Language parameter is unsupported for now.
34 // const char kLanguageString[] = "language";
35 const char kLocationString
[] = "location";
36 const char kSensorString
[] = "sensor";
37 const char kTimestampString
[] = "timestamp";
39 const char kDstOffsetString
[] = "dstOffset";
40 const char kRawOffsetString
[] = "rawOffset";
41 const char kTimeZoneIdString
[] = "timeZoneId";
42 const char kTimeZoneNameString
[] = "timeZoneName";
43 const char kStatusString
[] = "status";
44 const char kErrorMessageString
[] = "error_message";
46 // Sleep between timezone request retry on HTTP error.
47 const unsigned int kResolveTimeZoneRetrySleepOnServerErrorSeconds
= 5;
49 // Sleep between timezone request retry on bad server response.
50 const unsigned int kResolveTimeZoneRetrySleepBadResponseSeconds
= 10;
52 struct StatusString2Enum
{
54 TimeZoneResponseData::Status value
;
57 const StatusString2Enum statusString2Enum
[] = {
58 {"OK", TimeZoneResponseData::OK
},
59 {"INVALID_REQUEST", TimeZoneResponseData::INVALID_REQUEST
},
60 {"OVER_QUERY_LIMIT", TimeZoneResponseData::OVER_QUERY_LIMIT
},
61 {"REQUEST_DENIED", TimeZoneResponseData::REQUEST_DENIED
},
62 {"UNKNOWN_ERROR", TimeZoneResponseData::UNKNOWN_ERROR
},
63 {"ZERO_RESULTS", TimeZoneResponseData::ZERO_RESULTS
}, };
65 enum TimeZoneRequestEvent
{
66 // NOTE: Do not renumber these as that would confuse interpretation of
67 // previously logged data. When making changes, also update the enum list
68 // in tools/metrics/histograms/histograms.xml to keep it in sync.
69 TIMEZONE_REQUEST_EVENT_REQUEST_START
= 0,
70 TIMEZONE_REQUEST_EVENT_RESPONSE_SUCCESS
= 1,
71 TIMEZONE_REQUEST_EVENT_RESPONSE_NOT_OK
= 2,
72 TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY
= 3,
73 TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED
= 4,
75 // NOTE: Add entries only immediately above this line.
76 TIMEZONE_REQUEST_EVENT_COUNT
= 5
79 enum TimeZoneRequestResult
{
80 // NOTE: Do not renumber these as that would confuse interpretation of
81 // previously logged data. When making changes, also update the enum list
82 // in tools/metrics/histograms/histograms.xml to keep it in sync.
83 TIMEZONE_REQUEST_RESULT_SUCCESS
= 0,
84 TIMEZONE_REQUEST_RESULT_FAILURE
= 1,
85 TIMEZONE_REQUEST_RESULT_SERVER_ERROR
= 2,
86 TIMEZONE_REQUEST_RESULT_CANCELLED
= 3,
88 // NOTE: Add entries only immediately above this line.
89 TIMEZONE_REQUEST_RESULT_COUNT
= 4
92 // Too many requests (more than 1) mean there is a problem in implementation.
93 void RecordUmaEvent(TimeZoneRequestEvent event
) {
94 UMA_HISTOGRAM_ENUMERATION(
95 "TimeZone.TimeZoneRequest.Event", event
, TIMEZONE_REQUEST_EVENT_COUNT
);
98 void RecordUmaResponseCode(int code
) {
99 UMA_HISTOGRAM_SPARSE_SLOWLY("TimeZone.TimeZoneRequest.ResponseCode", code
);
102 // Slow timezone resolve leads to bad user experience.
103 void RecordUmaResponseTime(base::TimeDelta elapsed
, bool success
) {
105 UMA_HISTOGRAM_TIMES("TimeZone.TimeZoneRequest.ResponseSuccessTime",
108 UMA_HISTOGRAM_TIMES("TimeZone.TimeZoneRequest.ResponseFailureTime",
113 void RecordUmaResult(TimeZoneRequestResult result
, unsigned retries
) {
114 UMA_HISTOGRAM_ENUMERATION(
115 "TimeZone.TimeZoneRequest.Result", result
, TIMEZONE_REQUEST_RESULT_COUNT
);
116 UMA_HISTOGRAM_SPARSE_SLOWLY("TimeZone.TimeZoneRequest.Retries", retries
);
119 // Creates the request url to send to the server.
120 GURL
TimeZoneRequestURL(const GURL
& url
,
121 const Geoposition
& geoposition
,
123 std::string
query(url
.query());
124 query
+= base::StringPrintf(
125 "%s=%f,%f", kLocationString
, geoposition
.latitude
, geoposition
.longitude
);
126 if (url
== DefaultTimezoneProviderURL()) {
127 std::string api_key
= google_apis::GetAPIKey();
128 if (!api_key
.empty()) {
132 query
+= net::EscapeQueryParamValue(api_key
, true);
135 if (!geoposition
.timestamp
.is_null()) {
136 query
+= base::StringPrintf(
137 "&%s=%ld", kTimestampString
, geoposition
.timestamp
.ToTimeT());
140 query
+= kSensorString
;
142 query
+= (sensor
? "true" : "false");
144 GURL::Replacements replacements
;
145 replacements
.SetQueryStr(query
);
146 return url
.ReplaceComponents(replacements
);
149 void PrintTimeZoneError(const GURL
& server_url
,
150 const std::string
& message
,
151 TimeZoneResponseData
* timezone
) {
152 timezone
->status
= TimeZoneResponseData::REQUEST_ERROR
;
153 timezone
->error_message
=
154 base::StringPrintf("TimeZone provider at '%s' : %s.",
155 server_url
.GetOrigin().spec().c_str(),
157 LOG(WARNING
) << "TimeZoneRequest::GetTimeZoneFromResponse() : "
158 << timezone
->error_message
;
161 // Parses the server response body. Returns true if parsing was successful.
162 // Sets |*timezone| to the parsed TimeZone if a valid timezone was received,
163 // otherwise leaves it unchanged.
164 bool ParseServerResponse(const GURL
& server_url
,
165 const std::string
& response_body
,
166 TimeZoneResponseData
* timezone
) {
169 if (response_body
.empty()) {
170 PrintTimeZoneError(server_url
, "Server returned empty response", timezone
);
171 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY
);
174 VLOG(1) << "TimeZoneRequest::ParseServerResponse() : Parsing response "
177 // Parse the response, ignoring comments.
178 std::string error_msg
;
179 scoped_ptr
<base::Value
> response_value(base::JSONReader::ReadAndReturnError(
180 response_body
, base::JSON_PARSE_RFC
, NULL
, &error_msg
));
181 if (response_value
== NULL
) {
182 PrintTimeZoneError(server_url
, "JSONReader failed: " + error_msg
, timezone
);
183 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED
);
187 const base::DictionaryValue
* response_object
= NULL
;
188 if (!response_value
->GetAsDictionary(&response_object
)) {
189 PrintTimeZoneError(server_url
,
190 "Unexpected response type : " +
191 base::StringPrintf("%u", response_value
->GetType()),
193 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED
);
199 if (!response_object
->GetStringWithoutPathExpansion(kStatusString
, &status
)) {
200 PrintTimeZoneError(server_url
, "Missing status attribute.", timezone
);
201 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED
);
206 for (size_t i
= 0; i
< arraysize(statusString2Enum
); ++i
) {
207 if (status
!= statusString2Enum
[i
].string
)
210 timezone
->status
= statusString2Enum
[i
].value
;
217 server_url
, "Bad status attribute value: '" + status
+ "'", timezone
);
218 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED
);
222 const bool status_ok
= (timezone
->status
== TimeZoneResponseData::OK
);
224 if (!response_object
->GetDoubleWithoutPathExpansion(kDstOffsetString
,
225 &timezone
->dstOffset
) &&
227 PrintTimeZoneError(server_url
, "Missing dstOffset attribute.", timezone
);
228 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED
);
232 if (!response_object
->GetDoubleWithoutPathExpansion(kRawOffsetString
,
233 &timezone
->rawOffset
) &&
235 PrintTimeZoneError(server_url
, "Missing rawOffset attribute.", timezone
);
236 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED
);
240 if (!response_object
->GetStringWithoutPathExpansion(kTimeZoneIdString
,
241 &timezone
->timeZoneId
) &&
243 PrintTimeZoneError(server_url
, "Missing timeZoneId attribute.", timezone
);
244 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED
);
248 if (!response_object
->GetStringWithoutPathExpansion(
249 kTimeZoneNameString
, &timezone
->timeZoneName
) &&
251 PrintTimeZoneError(server_url
, "Missing timeZoneName attribute.", timezone
);
252 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED
);
256 // "error_message" field is optional. Ignore result.
257 response_object
->GetStringWithoutPathExpansion(kErrorMessageString
,
258 &timezone
->error_message
);
263 // Attempts to extract a position from the response. Detects and indicates
264 // various failure cases.
265 scoped_ptr
<TimeZoneResponseData
> GetTimeZoneFromResponse(
268 const std::string
& response_body
,
269 const GURL
& server_url
) {
270 scoped_ptr
<TimeZoneResponseData
> timezone(new TimeZoneResponseData
);
272 // HttpPost can fail for a number of reasons. Most likely this is because
273 // we're offline, or there was no response.
275 PrintTimeZoneError(server_url
, "No response received", timezone
.get());
276 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY
);
277 return timezone
.Pass();
279 if (status_code
!= net::HTTP_OK
) {
280 std::string message
= "Returned error code ";
281 message
+= base::IntToString(status_code
);
282 PrintTimeZoneError(server_url
, message
, timezone
.get());
283 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_NOT_OK
);
284 return timezone
.Pass();
287 if (!ParseServerResponse(server_url
, response_body
, timezone
.get()))
288 return timezone
.Pass();
290 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_SUCCESS
);
291 return timezone
.Pass();
296 TimeZoneResponseData::TimeZoneResponseData()
297 : dstOffset(0), rawOffset(0), status(ZERO_RESULTS
) {
300 GURL
DefaultTimezoneProviderURL() {
301 return GURL(kDefaultTimezoneProviderUrl
);
304 TimeZoneRequest::TimeZoneRequest(
305 net::URLRequestContextGetter
* url_context_getter
,
306 const GURL
& service_url
,
307 const Geoposition
& geoposition
,
309 base::TimeDelta retry_timeout
)
310 : url_context_getter_(url_context_getter
),
311 service_url_(service_url
),
312 geoposition_(geoposition
),
314 retry_timeout_abs_(base::Time::Now() + retry_timeout
),
315 retry_sleep_on_server_error_(base::TimeDelta::FromSeconds(
316 kResolveTimeZoneRetrySleepOnServerErrorSeconds
)),
317 retry_sleep_on_bad_response_(base::TimeDelta::FromSeconds(
318 kResolveTimeZoneRetrySleepBadResponseSeconds
)),
322 TimeZoneRequest::~TimeZoneRequest() {
323 DCHECK(thread_checker_
.CalledOnValidThread());
325 // If callback is not empty, request is cancelled.
326 if (!callback_
.is_null()) {
327 RecordUmaResponseTime(base::Time::Now() - request_started_at_
, false);
328 RecordUmaResult(TIMEZONE_REQUEST_RESULT_CANCELLED
, retries_
);
332 void TimeZoneRequest::StartRequest() {
333 DCHECK(thread_checker_
.CalledOnValidThread());
334 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_REQUEST_START
);
335 request_started_at_
= base::Time::Now();
339 net::URLFetcher::Create(request_url_
, net::URLFetcher::GET
, this));
340 url_fetcher_
->SetRequestContext(url_context_getter_
.get());
341 url_fetcher_
->SetLoadFlags(net::LOAD_BYPASS_CACHE
|
342 net::LOAD_DISABLE_CACHE
|
343 net::LOAD_DO_NOT_SAVE_COOKIES
|
344 net::LOAD_DO_NOT_SEND_COOKIES
|
345 net::LOAD_DO_NOT_SEND_AUTH_DATA
);
346 url_fetcher_
->Start();
349 void TimeZoneRequest::MakeRequest(TimeZoneResponseCallback callback
) {
350 callback_
= callback
;
352 TimeZoneRequestURL(service_url_
, geoposition_
, false /* sensor */);
356 void TimeZoneRequest::Retry(bool server_error
) {
357 const base::TimeDelta
delay(server_error
? retry_sleep_on_server_error_
358 : retry_sleep_on_bad_response_
);
359 timezone_request_scheduled_
.Start(
360 FROM_HERE
, delay
, this, &TimeZoneRequest::StartRequest
);
363 void TimeZoneRequest::OnURLFetchComplete(const net::URLFetcher
* source
) {
364 DCHECK_EQ(url_fetcher_
.get(), source
);
366 net::URLRequestStatus status
= source
->GetStatus();
367 int response_code
= source
->GetResponseCode();
368 RecordUmaResponseCode(response_code
);
371 source
->GetResponseAsString(&data
);
372 scoped_ptr
<TimeZoneResponseData
> timezone
= GetTimeZoneFromResponse(
373 status
.is_success(), response_code
, data
, source
->GetURL());
374 const bool server_error
=
375 !status
.is_success() || (response_code
>= 500 && response_code
< 600);
376 url_fetcher_
.reset();
378 DVLOG(1) << "TimeZoneRequest::OnURLFetchComplete(): timezone={"
379 << timezone
->ToStringForDebug() << "}";
381 const base::Time now
= base::Time::Now();
382 const bool retry_timeout
= (now
>= retry_timeout_abs_
);
384 const bool success
= (timezone
->status
== TimeZoneResponseData::OK
);
385 if (!success
&& !retry_timeout
) {
389 RecordUmaResponseTime(base::Time::Now() - request_started_at_
, success
);
391 const TimeZoneRequestResult result
=
392 (server_error
? TIMEZONE_REQUEST_RESULT_SERVER_ERROR
393 : (success
? TIMEZONE_REQUEST_RESULT_SUCCESS
394 : TIMEZONE_REQUEST_RESULT_FAILURE
));
395 RecordUmaResult(result
, retries_
);
397 TimeZoneResponseCallback callback
= callback_
;
399 // Empty callback is used to identify "completed or not yet started request".
402 // callback.Run() usually destroys TimeZoneRequest, because this is the way
403 // callback is implemented in TimeZoneProvider.
404 callback
.Run(timezone
.Pass(), server_error
);
405 // "this" is already destroyed here.
408 std::string
TimeZoneResponseData::ToStringForDebug() const {
409 static const char* const status2string
[] = {
419 return base::StringPrintf(
420 "dstOffset=%f, rawOffset=%f, timeZoneId='%s', timeZoneName='%s', "
421 "error_message='%s', status=%u (%s)",
425 timeZoneName
.c_str(),
426 error_message
.c_str(),
428 (status
< arraysize(status2string
) ? status2string
[status
] : "unknown"));
431 } // namespace chromeos