diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 814b9d7..61b8d13 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -136,7 +136,7 @@ Before submitting a PR, verify:
- [ ] Plugin builds without errors: `./gradlew clean jar`
- [ ] JAR uploads successfully to dotCMS
- [ ] OSGi bundle starts without errors (check logs for "Starting Google Analytics OSGI plugin")
-- [ ] Viewtool is available in Velocity (`$analytics`)
+- [ ] Viewtool is available in Velocity (`$googleanalytics`)
- [ ] Can create analytics request and query GA4 data
- [ ] No breaking changes to existing Velocity code (or documented if necessary)
- [ ] Works with dotCMS 23.01.10 and newer
diff --git a/README.md b/README.md
index 5fa2d36..c7b7178 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ OSGi plugin that integrates Google Analytics 4 (GA4) Data API with dotCMS, enabl
## What It Does
-This plugin provides a `$analytics` viewtool in Velocity templates for querying Google Analytics 4 data directly from your dotCMS pages. Retrieve metrics like sessions, active users, page views, and more—filtered by dimensions like date, page path, device category, etc.
+This plugin provides a `$googleanalytics` viewtool in Velocity templates for querying Google Analytics 4 data directly from your dotCMS pages. Retrieve metrics like sessions, active users, page views, and more—filtered by dimensions like date, page path, device category, etc.
**Note:** This plugin *fetches* analytics data from Google Analytics. It does NOT add tracking code to your site.
@@ -46,14 +46,14 @@ This plugin provides a `$analytics` viewtool in Velocity templates for querying
#set($propertyId = "123456789")
## Create and configure request
-#set($gaRequest = $analytics.createAnalyticsRequest($propertyId))
+#set($gaRequest = $googleanalytics.createAnalyticsRequest($propertyId))
$gaRequest.setStartDate("2026-02-09")
$gaRequest.setEndDate("2026-02-16")
$gaRequest.setMetrics("sessions,activeUsers")
$gaRequest.setDimensions("date")
## Execute query
-#set($gaResponse = $analytics.query($gaRequest))
+#set($gaResponse = $googleanalytics.query($gaRequest))
## Display results
@@ -72,6 +72,58 @@ $gaRequest.setDimensions("date")
```
+### REST API Usage
+
+Query Google Analytics data via REST endpoint:
+
+```bash
+curl -X POST http://localhost:8080/api/v1/googleanalytics/query \
+ -H "Content-Type: application/json" \
+ -u admin@dotcms.com:admin \
+ -d '{
+ "propertyId": "123456789",
+ "startDate": "2026-02-09",
+ "endDate": "2026-02-16",
+ "metrics": ["sessions", "activeUsers"],
+ "dimensions": ["date", "pagePath"],
+ "filters": {
+ "dimension": [
+ {"field": "pagePath", "value": "/products", "operator": "CONTAINS"}
+ ]
+ },
+ "sort": "sessions",
+ "maxResults": 100
+ }'
+```
+
+**Response:**
+
+```json
+{
+ "rowCount": 7,
+ "dimensions": ["date", "pagePath"],
+ "metrics": ["sessions", "activeUsers"],
+ "rows": [
+ {
+ "date": "20260209",
+ "pagePath": "/products",
+ "sessions": "150",
+ "activeUsers": "120"
+ },
+ {
+ "date": "20260210",
+ "pagePath": "/home",
+ "sessions": "200",
+ "activeUsers": "180"
+ }
+ ],
+ "metadata": {
+ "currencyCode": "USD",
+ "timeZone": "America/New_York"
+ }
+}
+```
+
## Documentation
For complete setup instructions including Google Cloud configuration, Google Analytics permissions, advanced usage, and troubleshooting:
@@ -86,16 +138,46 @@ For complete setup instructions including Google Cloud configuration, Google Ana
- **Troubleshooting** - OSGi issues, metric errors, variable name conflicts
- **Available Metrics & Dimensions** - GA4 API schema reference
-## Available Metrics
+## Understanding Dimensions and Metrics
+
+When querying Google Analytics, you combine **dimensions** and **metrics** to get the data you need:
+
+### Dimensions (What to group by)
-Common GA4 metrics you can query:
-- `sessions` - Number of sessions
+Dimensions are categorical attributes that describe your data—they answer "what are we breaking this down by?"
+
+Common dimensions:
+- `date` - When the activity happened (e.g., "20260209")
+- `pagePath` - Which page was viewed (e.g., "/products")
+- `country` - Where users are located (e.g., "United States")
+- `deviceCategory` - Device type (e.g., "desktop", "mobile", "tablet")
+- `browser` - Browser used (e.g., "Chrome", "Safari")
+- `city` - User's city (e.g., "New York")
+- `source` - Traffic source (e.g., "google", "facebook", "direct")
+
+### Metrics (What to measure)
+
+Metrics are the numerical measurements you want to analyze:
+- `sessions` - Number of sessions (visits)
- `activeUsers` - Number of distinct users
-- `screenPageViews` - Total page and screen views
+- `screenPageViews` - Total page views
- `bounceRate` - Percentage of single-page sessions
- `averageSessionDuration` - Average session duration in seconds
-See the [GA4 API Schema](https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema) for the full list.
+### Example Query
+
+"Show me sessions and active users, broken down by date and page path"
+
+```json
+{
+ "dimensions": ["date", "pagePath"],
+ "metrics": ["sessions", "activeUsers"]
+}
+```
+
+Each row in the response represents one unique combination of dimension values with its associated metrics.
+
+See the [GA4 API Schema](https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema) for the complete list of available dimensions and metrics.
## Version History
diff --git a/build.gradle b/build.gradle
index f1b9b66..66af656 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ plugins {
sourceCompatibility = JavaVersion.VERSION_11
-version = '0.4.1'
+version = '0.5.0'
repositories {
@@ -36,6 +36,7 @@ dependencies {
//compileOnly('org.apache.httpcomponents:httpclient:4.5.9')
// https://mvnrepository.com/artifact/javax.servlet/servlet-api
compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.5'
+ compileOnly group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.1.1'
}
diff --git a/src/main/java/com/dotcms/google/analytics/osgi/Activator.java b/src/main/java/com/dotcms/google/analytics/osgi/Activator.java
index 120204c..3966018 100644
--- a/src/main/java/com/dotcms/google/analytics/osgi/Activator.java
+++ b/src/main/java/com/dotcms/google/analytics/osgi/Activator.java
@@ -1,6 +1,7 @@
package com.dotcms.google.analytics.osgi;
import com.dotcms.google.analytics.app.AnalyticsAppService;
+import com.dotcms.google.analytics.rest.GoogleAnalyticsResource;
import com.dotcms.google.analytics.view.AnalyticsToolInfo;
import com.dotmarketing.business.CacheLocator;
import com.dotmarketing.loggers.Log4jUtil;
@@ -45,6 +46,9 @@ public final void start(final BundleContext bundleContext) throws Exception {
// copy the yaml
copyAppYml();
+ // Register REST resources
+ publishBundleServices(bundleContext);
+
// Register all ViewTool services
registerViewToolService(bundleContext, new AnalyticsToolInfo());
diff --git a/src/main/java/com/dotcms/google/analytics/rest/GoogleAnalyticsResource.java b/src/main/java/com/dotcms/google/analytics/rest/GoogleAnalyticsResource.java
new file mode 100644
index 0000000..1ff72d4
--- /dev/null
+++ b/src/main/java/com/dotcms/google/analytics/rest/GoogleAnalyticsResource.java
@@ -0,0 +1,300 @@
+package com.dotcms.google.analytics.rest;
+
+import com.dotcms.google.analytics.app.AnalyticsAppService;
+import com.dotcms.google.analytics.model.AnalyticsRequest;
+import com.dotcms.google.analytics.model.FilterRequest;
+import com.dotcms.google.analytics.service.GoogleAnalyticsService;
+import com.dotcms.rest.WebResource;
+import com.dotmarketing.beans.Host;
+import com.dotmarketing.business.web.WebAPILocator;
+import com.dotmarketing.util.Logger;
+import com.google.analytics.data.v1beta.DimensionValue;
+import com.google.analytics.data.v1beta.MetricValue;
+import com.google.analytics.data.v1beta.RunReportResponse;
+import com.liferay.portal.model.User;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * REST endpoint for querying Google Analytics data.
+ *
+ * @author dotCMS
+ */
+@Path("/v1/googleanalytics")
+public class GoogleAnalyticsResource {
+
+ private final WebResource webResource = new WebResource();
+ private final AnalyticsAppService analyticsAppService = new AnalyticsAppService();
+ private final Map googleAnalyticsServiceMap = new ConcurrentHashMap<>();
+
+ /**
+ * Query Google Analytics 4 data via REST API.
+ *
+ * Example request:
+ * POST /api/v1/googleanalytics/query
+ * {
+ * "propertyId": "123456789",
+ * "startDate": "2026-02-09",
+ * "endDate": "2026-02-16",
+ * "metrics": ["sessions", "activeUsers"],
+ * "dimensions": ["date"],
+ * "maxResults": 100
+ * }
+ *
+ * @param request HTTP request
+ * @param response HTTP response
+ * @param queryRequest Analytics query parameters
+ * @return JSON response with analytics data
+ */
+ @POST
+ @Path("/query")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response query(
+ @Context final HttpServletRequest request,
+ @Context final HttpServletResponse response,
+ final GoogleAnalyticsQueryRequest queryRequest) {
+
+ try {
+ // Validate request body
+ if (queryRequest == null) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity(Map.of("error", "Request body is required"))
+ .build();
+ }
+
+ // Authenticate user
+ final User user = new WebResource.InitBuilder(webResource)
+ .requiredBackendUser(true)
+ .requiredFrontendUser(false)
+ .requestAndResponse(request, response)
+ .rejectWhenNoUser(true)
+ .init()
+ .getUser();
+
+ Logger.debug(this, () -> "User authenticated: " + user.getEmailAddress());
+
+ // Validate request
+ if (queryRequest.getPropertyId() == null || queryRequest.getPropertyId().isEmpty()) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity(Map.of("error", "propertyId is required"))
+ .build();
+ }
+
+ // Get analytics app configuration for current site
+ final Host currentHost = WebAPILocator.getHostWebAPI().getHost(request);
+ final String siteId = currentHost.getIdentifier();
+
+ // Get or create cached Google Analytics service
+ final GoogleAnalyticsService analyticsService =
+ this.googleAnalyticsServiceMap.computeIfAbsent(siteId, key -> {
+ try {
+ final AnalyticsApp analyticsApp = analyticsAppService.getAnalyticsApp(siteId);
+ return new GoogleAnalyticsService(analyticsApp.getJsonKeyFile());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ // Build analytics request
+ final AnalyticsRequest analyticsRequest =
+ new AnalyticsRequest(queryRequest.getPropertyId());
+
+ // Set date range
+ if (queryRequest.getStartDate() != null) {
+ analyticsRequest.setStartDate(queryRequest.getStartDate());
+ }
+ if (queryRequest.getEndDate() != null) {
+ analyticsRequest.setEndDate(queryRequest.getEndDate());
+ }
+
+ // Set metrics
+ if (queryRequest.getMetrics() != null && !queryRequest.getMetrics().isEmpty()) {
+ analyticsRequest.setMetrics(String.join(",", queryRequest.getMetrics()));
+ }
+
+ // Set dimensions
+ if (queryRequest.getDimensions() != null && !queryRequest.getDimensions().isEmpty()) {
+ analyticsRequest.setDimensions(String.join(",", queryRequest.getDimensions()));
+ }
+
+ // Set filters
+ if (queryRequest.getFilters() != null) {
+ if (queryRequest.getFilters().getDimension() != null) {
+ for (FilterRequestDTO filter : queryRequest.getFilters().getDimension()) {
+ final FilterRequest filterRequest = new FilterRequest(
+ filter.getField(),
+ filter.getOperator(),
+ filter.getValue()
+ );
+ analyticsRequest.getDimensionFilterList().add(filterRequest);
+ }
+ }
+
+ if (queryRequest.getFilters().getMetric() != null) {
+ for (FilterRequestDTO filter : queryRequest.getFilters().getMetric()) {
+ final FilterRequest filterRequest = new FilterRequest(
+ filter.getField(),
+ filter.getOperator(),
+ filter.getValue()
+ );
+ analyticsRequest.getMetricFilterList().add(filterRequest);
+ }
+ }
+ }
+
+ // Set sort
+ if (queryRequest.getSort() != null) {
+ analyticsRequest.setSort(queryRequest.getSort());
+ }
+
+ // Set max results
+ if (queryRequest.getMaxResults() != null && queryRequest.getMaxResults() > 0) {
+ analyticsRequest.setMaxResults(queryRequest.getMaxResults());
+ }
+
+ // Execute query
+ final RunReportResponse gaResponse = analyticsService.query(analyticsRequest);
+
+ // Capture field names for flattened response
+ final List dimensionNames = queryRequest.getDimensions();
+ final List metricNames = queryRequest.getMetrics();
+
+ // Convert to JSON-friendly format
+ final Map responseData = new HashMap<>();
+ responseData.put("rowCount", gaResponse.getRowCount());
+
+ // Add metadata with dimension and metric names
+ responseData.put("dimensions", dimensionNames != null ? dimensionNames : new ArrayList<>());
+ responseData.put("metrics", metricNames != null ? metricNames : new ArrayList<>());
+
+ // Convert rows to flattened structure with named fields
+ final List