Skip to content

Commit f4db711

Browse files
fmontesclaude
andcommitted
Add REST endpoint for headless Google Analytics queries
Adds /api/v1/googleanalytics/query endpoint for headless/JavaScript clients. ## Changes - New REST resource at GoogleAnalyticsResource.java - POST /api/v1/googleanalytics/query endpoint - Accepts JSON request with propertyId, dates, metrics, dimensions, filters, sort, maxResults - Returns JSON-friendly response format (rows, dimensions, metrics, metadata) - Requires backend user authentication - Registers REST resource in Activator ## Example Request ```json { "propertyId": "123456789", "startDate": "2026-02-09", "endDate": "2026-02-16", "metrics": ["sessions", "activeUsers"], "dimensions": ["date"], "maxResults": 100 } ``` Version bumped to 0.5.0 (minor version for new feature). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent f9c81c7 commit f4db711

3 files changed

Lines changed: 273 additions & 1 deletion

File tree

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ plugins {
88

99

1010
sourceCompatibility = JavaVersion.VERSION_11
11-
version = '0.4.1'
11+
version = '0.5.0'
1212

1313

1414
repositories {
@@ -36,6 +36,7 @@ dependencies {
3636
//compileOnly('org.apache.httpcomponents:httpclient:4.5.9')
3737
// https://mvnrepository.com/artifact/javax.servlet/servlet-api
3838
compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.5'
39+
compileOnly group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.1.1'
3940

4041
}
4142

src/main/java/com/dotcms/google/analytics/osgi/Activator.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.dotcms.google.analytics.osgi;
22

33
import com.dotcms.google.analytics.app.AnalyticsAppService;
4+
import com.dotcms.google.analytics.rest.GoogleAnalyticsResource;
45
import com.dotcms.google.analytics.view.AnalyticsToolInfo;
56
import com.dotmarketing.business.CacheLocator;
67
import com.dotmarketing.loggers.Log4jUtil;
@@ -45,6 +46,9 @@ public final void start(final BundleContext bundleContext) throws Exception {
4546
// copy the yaml
4647
copyAppYml();
4748

49+
// Register REST resources
50+
publishBundleServices(bundleContext);
51+
4852
// Register all ViewTool services
4953
registerViewToolService(bundleContext, new AnalyticsToolInfo());
5054

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
package com.dotcms.google.analytics.rest;
2+
3+
import com.dotcms.google.analytics.app.AnalyticsApp;
4+
import com.dotcms.google.analytics.app.AnalyticsAppService;
5+
import com.dotcms.google.analytics.model.AnalyticsRequest;
6+
import com.dotcms.google.analytics.model.FilterRequest;
7+
import com.dotcms.google.analytics.service.GoogleAnalyticsService;
8+
import com.dotcms.rest.WebResource;
9+
import com.dotmarketing.util.Logger;
10+
import com.google.analytics.data.v1beta.RunReportResponse;
11+
import com.liferay.portal.model.User;
12+
13+
import javax.servlet.http.HttpServletRequest;
14+
import javax.servlet.http.HttpServletResponse;
15+
import javax.ws.rs.POST;
16+
import javax.ws.rs.Path;
17+
import javax.ws.rs.Produces;
18+
import javax.ws.rs.core.Context;
19+
import javax.ws.rs.core.MediaType;
20+
import javax.ws.rs.core.Response;
21+
import java.util.ArrayList;
22+
import java.util.HashMap;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.stream.Collectors;
26+
27+
/**
28+
* REST endpoint for querying Google Analytics data.
29+
*
30+
* @author dotCMS
31+
*/
32+
@Path("/v1/googleanalytics")
33+
public class GoogleAnalyticsResource {
34+
35+
private final WebResource webResource = new WebResource();
36+
private final AnalyticsAppService analyticsAppService = new AnalyticsAppService();
37+
38+
/**
39+
* Query Google Analytics 4 data via REST API.
40+
*
41+
* Example request:
42+
* POST /api/v1/googleanalytics/query
43+
* {
44+
* "propertyId": "123456789",
45+
* "startDate": "2026-02-09",
46+
* "endDate": "2026-02-16",
47+
* "metrics": ["sessions", "activeUsers"],
48+
* "dimensions": ["date"],
49+
* "maxResults": 100
50+
* }
51+
*
52+
* @param request HTTP request
53+
* @param response HTTP response
54+
* @param queryRequest Analytics query parameters
55+
* @return JSON response with analytics data
56+
*/
57+
@POST
58+
@Path("/query")
59+
@Produces(MediaType.APPLICATION_JSON)
60+
public Response query(
61+
@Context final HttpServletRequest request,
62+
@Context final HttpServletResponse response,
63+
final GoogleAnalyticsQueryRequest queryRequest) {
64+
65+
try {
66+
// Authenticate user
67+
final User user = new WebResource.InitBuilder(webResource)
68+
.requiredBackendUser(true)
69+
.requiredFrontendUser(false)
70+
.requestAndResponse(request, response)
71+
.rejectWhenNoUser(true)
72+
.init()
73+
.getUser();
74+
75+
Logger.debug(this, () -> "User authenticated: " + user.getEmailAddress());
76+
77+
// Validate request
78+
if (queryRequest.getPropertyId() == null || queryRequest.getPropertyId().isEmpty()) {
79+
return Response.status(Response.Status.BAD_REQUEST)
80+
.entity(Map.of("error", "propertyId is required"))
81+
.build();
82+
}
83+
84+
// Get analytics app configuration for current site
85+
final String siteId = request.getServerName(); // Or extract from request
86+
final AnalyticsApp analyticsApp = analyticsAppService.getAnalyticsApp(siteId);
87+
88+
// Create Google Analytics service
89+
final GoogleAnalyticsService analyticsService =
90+
new GoogleAnalyticsService(analyticsApp.getJsonKeyFile());
91+
92+
// Build analytics request
93+
final AnalyticsRequest analyticsRequest =
94+
new AnalyticsRequest(queryRequest.getPropertyId());
95+
96+
// Set date range
97+
if (queryRequest.getStartDate() != null) {
98+
analyticsRequest.setStartDate(queryRequest.getStartDate());
99+
}
100+
if (queryRequest.getEndDate() != null) {
101+
analyticsRequest.setEndDate(queryRequest.getEndDate());
102+
}
103+
104+
// Set metrics
105+
if (queryRequest.getMetrics() != null && !queryRequest.getMetrics().isEmpty()) {
106+
analyticsRequest.setMetrics(String.join(",", queryRequest.getMetrics()));
107+
}
108+
109+
// Set dimensions
110+
if (queryRequest.getDimensions() != null && !queryRequest.getDimensions().isEmpty()) {
111+
analyticsRequest.setDimensions(String.join(",", queryRequest.getDimensions()));
112+
}
113+
114+
// Set filters
115+
if (queryRequest.getFilters() != null) {
116+
if (queryRequest.getFilters().getDimension() != null) {
117+
for (FilterRequestDTO filter : queryRequest.getFilters().getDimension()) {
118+
final FilterRequest filterRequest = new FilterRequest(
119+
filter.getField(),
120+
filter.getOperator(),
121+
filter.getValue()
122+
);
123+
analyticsRequest.getDimensionFilterList().add(filterRequest);
124+
}
125+
}
126+
127+
if (queryRequest.getFilters().getMetric() != null) {
128+
for (FilterRequestDTO filter : queryRequest.getFilters().getMetric()) {
129+
final FilterRequest filterRequest = new FilterRequest(
130+
filter.getField(),
131+
filter.getOperator(),
132+
filter.getValue()
133+
);
134+
analyticsRequest.getMetricFilterList().add(filterRequest);
135+
}
136+
}
137+
}
138+
139+
// Set sort
140+
if (queryRequest.getSort() != null) {
141+
analyticsRequest.setSort(queryRequest.getSort());
142+
}
143+
144+
// Set max results
145+
if (queryRequest.getMaxResults() != null && queryRequest.getMaxResults() > 0) {
146+
analyticsRequest.setMaxResults(queryRequest.getMaxResults());
147+
}
148+
149+
// Execute query
150+
final RunReportResponse gaResponse = analyticsService.query(analyticsRequest);
151+
152+
// Convert to JSON-friendly format
153+
final Map<String, Object> responseData = new HashMap<>();
154+
responseData.put("rowCount", gaResponse.getRowCount());
155+
156+
// Convert rows to simple structure
157+
final List<Map<String, Object>> rows = gaResponse.getRowsList().stream()
158+
.map(row -> {
159+
final Map<String, Object> rowData = new HashMap<>();
160+
161+
// Extract dimensions
162+
final List<String> dimensions = row.getDimensionValuesList().stream()
163+
.map(dv -> dv.getValue())
164+
.collect(Collectors.toList());
165+
rowData.put("dimensions", dimensions);
166+
167+
// Extract metrics
168+
final List<String> metrics = row.getMetricValuesList().stream()
169+
.map(mv -> mv.getValue())
170+
.collect(Collectors.toList());
171+
rowData.put("metrics", metrics);
172+
173+
return rowData;
174+
})
175+
.collect(Collectors.toList());
176+
177+
responseData.put("rows", rows);
178+
179+
// Add metadata
180+
final Map<String, String> metadata = new HashMap<>();
181+
if (gaResponse.getMetadata() != null) {
182+
metadata.put("currencyCode", gaResponse.getMetadata().getCurrencyCode());
183+
metadata.put("timeZone", gaResponse.getMetadata().getTimeZone());
184+
}
185+
responseData.put("metadata", metadata);
186+
187+
return Response.ok(responseData).build();
188+
189+
} catch (Exception e) {
190+
Logger.error(this, "Error querying Google Analytics", e);
191+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
192+
.entity(Map.of("error", e.getMessage()))
193+
.build();
194+
}
195+
}
196+
197+
/**
198+
* Request DTO for Google Analytics query.
199+
*/
200+
public static class GoogleAnalyticsQueryRequest {
201+
private String propertyId;
202+
private String startDate;
203+
private String endDate;
204+
private List<String> metrics;
205+
private List<String> dimensions;
206+
private FiltersDTO filters;
207+
private String sort;
208+
private Integer maxResults;
209+
210+
// Getters and setters
211+
public String getPropertyId() { return propertyId; }
212+
public void setPropertyId(String propertyId) { this.propertyId = propertyId; }
213+
214+
public String getStartDate() { return startDate; }
215+
public void setStartDate(String startDate) { this.startDate = startDate; }
216+
217+
public String getEndDate() { return endDate; }
218+
public void setEndDate(String endDate) { this.endDate = endDate; }
219+
220+
public List<String> getMetrics() { return metrics; }
221+
public void setMetrics(List<String> metrics) { this.metrics = metrics; }
222+
223+
public List<String> getDimensions() { return dimensions; }
224+
public void setDimensions(List<String> dimensions) { this.dimensions = dimensions; }
225+
226+
public FiltersDTO getFilters() { return filters; }
227+
public void setFilters(FiltersDTO filters) { this.filters = filters; }
228+
229+
public String getSort() { return sort; }
230+
public void setSort(String sort) { this.sort = sort; }
231+
232+
public Integer getMaxResults() { return maxResults; }
233+
public void setMaxResults(Integer maxResults) { this.maxResults = maxResults; }
234+
}
235+
236+
/**
237+
* Filters container DTO.
238+
*/
239+
public static class FiltersDTO {
240+
private List<FilterRequestDTO> dimension;
241+
private List<FilterRequestDTO> metric;
242+
243+
public List<FilterRequestDTO> getDimension() { return dimension; }
244+
public void setDimension(List<FilterRequestDTO> dimension) { this.dimension = dimension; }
245+
246+
public List<FilterRequestDTO> getMetric() { return metric; }
247+
public void setMetric(List<FilterRequestDTO> metric) { this.metric = metric; }
248+
}
249+
250+
/**
251+
* Filter DTO.
252+
*/
253+
public static class FilterRequestDTO {
254+
private String field;
255+
private String value;
256+
private String operator;
257+
258+
public String getField() { return field; }
259+
public void setField(String field) { this.field = field; }
260+
261+
public String getValue() { return value; }
262+
public void setValue(String value) { this.value = value; }
263+
264+
public String getOperator() { return operator; }
265+
public void setOperator(String operator) { this.operator = operator; }
266+
}
267+
}

0 commit comments

Comments
 (0)