// Copyright 2009 Google Inc. All Rights Reserved.
package spanishgringo.data;
import com.google.gdata.client.analytics.AnalyticsService;
import com.google.gdata.client.analytics.DataQuery;
import com.google.gdata.data.analytics.AccountEntry;
import com.google.gdata.data.analytics.AccountFeed;
import com.google.gdata.data.analytics.DataEntry;
import com.google.gdata.data.analytics.DataFeed;
import com.google.gdata.data.analytics.Segment;
import com.google.gdata.util.ServiceException;
import com.google.gdata.data.analytics.Metric;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
/**
* Makes all requests to the Google Analytics Data Export API.
* @author api.nickm@google.com (Nick Mihailovski)
*/
public class AnalyticsServiceWrapper {
/** The URL all Google Analytics Data Feed requests start with */
private static final String BASE_DATA_FEED_URL = "https://www.google.com/analytics/feeds/data";
private AnalyticsService analyticsService;
private AuthorizationService authService;
private String tableId;
private String profileName;
private String startDate;
private String endDate;
private String segment;
private String segmentName;
private String queryURL;
private Boolean tokenValid;
private String accountListError;
private String dataListError;
private static final Logger log = Logger.getLogger("AnalyticsServiceWrapper");
/**
* Constructor.
* @param analyticsService The AnalyticsService object to make requests to the
* Google Analytics API.
* @param authService The AuthorizationService implementation used to add the authorization
* token into the Google Analytics service object.
* @param tableId the ids parameter of the Data Feed query that corresponds to the Google
* Analytics tableId.
*/
public AnalyticsServiceWrapper(AnalyticsService analyticsService,
AuthorizationService authService, String tableId, String startDate, String endDate, String segment, String segmentName) {
this.analyticsService = analyticsService;
this.authService = authService;
this.tableId = tableId;
this.startDate = startDate;
this.endDate = endDate;
this.segment=segment;
this.segmentName=segmentName;
this.profileName = "";
// Increase the request timeout to 10 seconds for App Engine.
analyticsService.setConnectTimeout(10000);
}
/**
* Constructor.
* @param analyticsService The AnalyticsService object to make requests to the
* Google Analytics API.
* @param authService The AuthorizationService implementation used to add the authorization
* token into the Google Analytics service object.
* @param tableId the ids parameter of the Data Feed query that corresponds to the Google
* Analytics tableId.
*/
public AnalyticsServiceWrapper(AnalyticsService analyticsService,
AuthorizationService authService, String tableId, String startDate, String endDate) {
new AnalyticsServiceWrapper(analyticsService, authService, tableId, startDate, endDate, null, "Unknown");
}
/**
* Sets the token into the analytics service object. The logic to set the token is in the
* AuhtorizationSerivice because each authorization implementation handles setting the
* token differently.
*/
public void setToken(UserToken userToken) {
tokenValid = false;
if (userToken.hasSessionToken()) {
authService.putTokenInGoogleService(userToken, analyticsService);
// Assume the token is valid.
tokenValid = true;
}
}
/**
* Returns the table id.
* @return the table id.
*/
public String getTableId() {
return tableId;
}
/**
* Returns if the token was valid.
* @return if the token was valid.
*/
public Boolean isTokenValid() {
return tokenValid;
}
/**
* Returns a URL to query the Google Analytics API account feed. This query retrieves the 1000
* 1000 accounts the current authorized user has access to.
* @return the account feed URL.
* @throws MalformedURLException if the URL isn't correct formed.
*/
private URL getAccountFeedQuery() throws MalformedURLException {
return new URL("https://www.google.com/analytics/feeds/accounts/default");
}
/**
* Requests the Google Analytics account information profiles for the currently authorized user.
* Then extract the the profile name and table id into a list.
* @return a list of string arrays with the data from the API.
*/
public List<String []> getAccountList() {
if (!isTokenValid()){
return null;
}
Boolean firstEntry = true;
List<String []> accountList = new ArrayList<String[]>();
try {
// Make a request to the Account Feed.
AccountFeed accountFeed = analyticsService.getFeed(getAccountFeedQuery(), AccountFeed.class);
// Put the results in a list of String arrays.
for (AccountEntry entry : accountFeed.getEntries()) {
accountList.add(new String[] {
entry.getTableId().getValue(),
entry.getTitle().getPlainText()
});
// If no tableId has been configured, use the first tableId from the
// Account feed as a default.
if (firstEntry && tableId == null) {
firstEntry = false;
tableId = entry.getTableId().getValue();
}
}
} catch (ServiceException e) {
// If the token in the data store has been revoked, a 401 error is thrown. To continue,
// the user can just request another token from the authorization routine. So no error
// message is sent to the user.
if (e.getHttpErrorCodeOverride() == 401) {
tokenValid = false;
} else {
String errMsg = e.getMessage();
errMsg = errMsg.startsWith("Timeout")? errMsg +"<br/><strong><em>Timeouts can occur due to AppEngine's limit on query length (10 seconds). Please try again.</em></strong>":errMsg;
accountListError = errMsg;
}
} catch (IOException e) {
String errMsg = e.getMessage();
errMsg = errMsg.startsWith("Timeout")? errMsg +"<br/><strong><em>Timeouts can occur due to AppEngine's limit on query length (10 seconds). Please try again.</em></strong>":errMsg;
accountListError = errMsg;
}
return accountList;
}
/**
* Requests the Google Analytics account information for the profile given
* Then extract the the profile name and table id into a list.
* @return a list of string arrays with the data from the API.
*/
public List<String []> getAccountList(String gaID) {
if (!isTokenValid()){
return null;
}
Boolean firstEntry = true;
List<String []> accountList = new ArrayList<String[]>();
try {
// Make a request to the Account Feed.
AccountFeed accountFeed = analyticsService.getFeed(getAccountFeedQuery(), AccountFeed.class);
// Put the results in a list of String arrays.
for (AccountEntry entry : accountFeed.getEntries()) {
if( entry.getTableId().getValue().toString().equals(gaID)) {
accountList.add(new String[] {
entry.getTableId().getValue(),
entry.getTitle().getPlainText()
});
}
// If no tableId has been configured, use the first tableId from the
// Account feed as a default.
if (firstEntry && tableId == null) {
firstEntry = false;
tableId = entry.getTableId().getValue();
}
}
} catch (ServiceException e) {
// If the token in the data store has been revoked, a 401 error is thrown. To continue,
// the user can just request another token from the authorization routine. So no error
// message is sent to the user.
if (e.getHttpErrorCodeOverride() == 401) {
tokenValid = false;
} else {
accountListError = e.getMessage();
}
} catch (IOException e) {
accountListError = e.getMessage();
}
return accountList;
}
/**
* Requests the Google Analytics account information profiles for the currently authorized user.
* Then extract the the profile name, table id, Account Name and Account Id into a a list of GaProfile objects.
* @return a list of GaProfile objects with the data from the API.
*/
public List<GaProfile> getAccountList(boolean full) {
if (!isTokenValid()){
return null;
}
Boolean firstEntry = true;
List<GaProfile> accountList = new ArrayList<GaProfile>();
try {
// Make a request to the Account Feed.
AccountFeed accountFeed = analyticsService.getFeed(getAccountFeedQuery(), AccountFeed.class);
// Put the results in a list of String arrays.
for (AccountEntry entry : accountFeed.getEntries()) {
accountList.add(new GaProfile(
entry.getTableId().getValue(),
entry.getTitle().getPlainText(),
entry.getProperty("ga:accountName"),
entry.getProperty("ga:accountId")
)
);
// If no tableId has been configured, use the first tableId from the
// Account feed as a default.
if (firstEntry && tableId == null) {
firstEntry = false;
tableId = entry.getTableId().getValue();
}
}
} catch (ServiceException e) {
// If the token in the data store has been revoked, a 401 error is thrown. To continue,
// the user can just request another token from the authorization routine. So no error
// message is sent to the user.
if (e.getHttpErrorCodeOverride() == 401) {
tokenValid = false;
} else {
accountListError = e.getMessage();
}
} catch (IOException e) {
accountListError = e.getMessage();
}
return accountList;
}
/**
* Requests the Google Analytics account information profiles for the currently authorized user.
* Then extract the the profile name, table id, Account Name and Account Id into a a list of GaProfile objects.
* @return a list of GaProfile objects with the data from the API.
*/
public List<String []> getSegmentList() {
if (!isTokenValid()){
return null;
}
Boolean firstEntry = true;
List<String []> segmentList = new ArrayList<String []>();
try {
// Make a request to the Account Feed.
AccountFeed accountFeed = analyticsService.getFeed(getAccountFeedQuery(), AccountFeed.class);
// Put the results in a list of String arrays.
for (Segment curSegment : accountFeed.getSegments()) {
segmentList.add(new String[] {
curSegment.getId(),
curSegment.getName(),(curSegment.hasDefinition() ?
curSegment.getDefinition().toString():""),
});
}
} catch (ServiceException e) {
// If the token in the data store has been revoked, a 401 error is thrown. To continue,
// the user can just request another token from the authorization routine. So no error
// message is sent to the user.
if (e.getHttpErrorCodeOverride() == 401) {
tokenValid = false;
} else {
accountListError = e.getMessage();
}
} catch (IOException e) {
accountListError = e.getMessage();
}
return segmentList;
}
/**
* Returns a URL to query the Google Analytics API Data Feed. This query retrieves the top 100
* landing pages and their entrance and bounce metrics sorted by entrances for the last 14 days
* starting yesterday.
* @return a Google Analytics API query.
* @throws MalformedURLException
*/
private URL getDataFeedQuery() throws MalformedURLException {
// Formatter for date.
SimpleDateFormat gaDate = new SimpleDateFormat("yyyy-MM-dd");
Calendar lastMonth = Calendar.getInstance();
if(startDate==null) {
lastMonth.add(Calendar.MONTH, -1);
lastMonth.set(Calendar.DAY_OF_MONTH, 1);
startDate = gaDate.format(lastMonth.getTime());
lastMonth.set(Calendar.DAY_OF_MONTH, lastMonth.getActualMaximum(Calendar.DAY_OF_MONTH));
endDate = gaDate.format(lastMonth.getTime());
}
// Make a query.
DataQuery query = new DataQuery(new URL(BASE_DATA_FEED_URL));
query.setIds(tableId);
query.setDimensions("ga:year,ga:month");
query.setMetrics("ga:visits,ga:transactions,ga:itemQuantity,ga:uniquePageviews,ga:bounces,ga:entrances");
query.setSort("ga:year,ga:month");
query.setMaxResults(500);
query.setStartDate(startDate);
query.setEndDate(endDate);
if (segment !=null) {
query.setSegment(segment);
}
log.info(query.getUrl().toString());
String theURL = query.getUrl().toString();
return query.getUrl();
}
/**
* Requests the Google Analytics profile information for the table id set in this object. If no
* table id has been set, the table id of the first account retrieved in the setAccountList
* method is used. This method then extracts the the top 10 ga:source, ga:medium dimensions
* along with the ga:visits, ga:bouces metrics and sets them in this object's dataList member.
* @return a list of string arrays with the data from the API.
*/
public List<String []> getDataList() {
List<String []> dataList = new ArrayList<String[]>();
Double bounceRate = 0.0;
Double eComRate = 0.0;
String myVisitsCI = "";
if (!isTokenValid()) {
return dataList;
}
try {
//request account feed to get profile name
String[] profileList = getAccountList(tableId).get(0);
setProfileName(profileList[1]);
setQueryURL(getDataFeedQuery().toString());
// Make a request to the Data Feed.
DataFeed dataFeed = analyticsService.getFeed(getDataFeedQuery(), DataFeed.class);
dataList.add(new String[] {"Year", "Month", "Visits", "Transactions", "Quantity", "Unique Pg.V", "Bounce Rate", "eCommerce Conv.", "CI"});
NumberFormat f = NumberFormat.getInstance(Locale.US);
if (f instanceof DecimalFormat) {
((DecimalFormat) f).applyPattern("###.#");
}
// Put the results in a list of String arrays.
for (DataEntry entry : dataFeed.getEntries()) {
// Calculate bounce rate.
bounceRate = entry.doubleValueOf("ga:bounces") / entry.doubleValueOf("ga:entrances") * 100;
eComRate = entry.doubleValueOf("ga:transactions") / entry.doubleValueOf("ga:visits") * 100;
Metric myVisits = entry.getMetric("ga:visits");
myVisitsCI =String.valueOf(f.format(myVisits.getConfidenceInterval()));
dataList.add(new String[] {
entry.stringValueOf("ga:year"),
entry.stringValueOf("ga:month"),
entry.stringValueOf("ga:visits"),
entry.stringValueOf("ga:transactions"),
entry.stringValueOf("ga:itemQuantity"),
entry.stringValueOf("ga:uniquePageviews"),
Double.toString(bounceRate),
Double.toString(eComRate),
myVisitsCI
});
}
} catch (ServiceException e) {
dataListError = e.getMessage();
} catch (IOException e) {
String errMsg = e.getMessage();
errMsg = errMsg.startsWith("Timeout")? "<strong>"+errMsg.substring(0, errMsg.indexOf("http")) + "</strong>"+ errMsg.substring(errMsg.indexOf("http"))+"<br/><strong><em>This is common for long or complex queries due to AppEngine's limit on query length (10 seconds). GA caches results so please wait a few seconds & try again.</em></strong>":errMsg;
dataListError = errMsg;
}
catch (Exception e) {
dataListError = e.getMessage();
}
return dataList;
}
/**
* Returns the message from any errors encountered by retrieving account list.
* @return the error encountered by retrieving the account list.
*/
public String getAccountListError() {
return accountListError;
}
/**
* Returns the message from any errors encountered by retrieving the data list.
* @return the error encountered by retrieving the data list.
*/
public String getDataListError() {
return dataListError;
}
/**
* @param startDate the startDate to set
*/
public void setStartDate(String startDate) {
this.startDate = startDate;
}
/**
* @return the startDate
*/
public String getStartDate() {
return startDate;
}
/**
* @param endDate the endDate to set
*/
public void setEndDate(String endDate) {
this.endDate = endDate;
}
/**
* @return the endDate
*/
public String getEndDate() {
return endDate;
}
public void setSegment(String segment) {
this.segment = segment;
}
/**
* @return the endDate
*/
public String getSegment() {
return segment;
}
/**
* @param profileName the profileName to set
*/
public void setProfileName(String profileName) {
this.profileName = profileName;
}
/**
* @return the profileName
*/
public String getProfileName() {
return profileName;
}
/**
* @param queryURL the queryURL to set
*/
public void setQueryURL(String queryURL) {
this.queryURL = queryURL;
}
/**
* @return the queryURL
*/
public String getQueryURL() {
return queryURL;
}
/**
* @param segmentName the segmentName to set
*/
public void setSegmentName(String segmentName) {
this.segmentName = segmentName;
}
/**
* @return the segmentName
*/
public String getSegmentName() {
return segmentName;
}
}