Skip to content

Add REST endpoint for headless Google Analytics queries#3

Open
fmontes wants to merge 2 commits intomainfrom
feature/rest-endpoint
Open

Add REST endpoint for headless Google Analytics queries#3
fmontes wants to merge 2 commits intomainfrom
feature/rest-endpoint

Conversation

@fmontes
Copy link
Member

@fmontes fmontes commented Feb 16, 2026

Overview

Adds a REST endpoint at /api/v1/googleanalytics/query for querying Google Analytics 4 data programmatically, enabling headless/API-first use cases beyond Velocity templates.

What Changed

New REST Endpoint

Endpoint: POST /api/v1/googleanalytics/query

Request:

{
  "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 (Flattened):

{
  "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"
  }
}

Response Format

The endpoint returns a flattened, self-documenting response where:

  • dimensions array lists dimension field names
  • metrics array lists metric field names
  • Each row is a named object with field names matching requested dimensions/metrics

This eliminates the need for consumers to track positional array indices and provides better developer experience.

Documentation Updates

  • Added REST API usage section with curl examples
  • Added "Understanding Dimensions and Metrics" explainer
  • Clarified difference between dimensions (categorical grouping) and metrics (numerical measurements)

Why This Matters

Current limitation: The $analytics viewtool only works in Velocity templates, blocking:

  • SPAs/headless frontends that need analytics data via API
  • External integrations and dashboards
  • Mobile apps
  • Third-party services

This enables:

  • React/Vue/Angular apps to fetch GA4 data client-side
  • Headless CMS architectures
  • Custom analytics dashboards
  • API-driven integrations

Testing

Build

./gradlew clean jar
# Successfully builds to build/libs/google-analytics-0.5.0.jar

Manual Test

curl -X POST http://localhost:8080/api/v1/googleanalytics/query   -H "Content-Type: application/json"   -u admin@dotcms.com:admin   -d '{
    "propertyId": "YOUR_PROPERTY_ID",
    "startDate": "2026-02-09",
    "endDate": "2026-02-16",
    "metrics": ["sessions", "activeUsers"],
    "dimensions": ["date"]
  }'

Expected: JSON response with flattened row structure and named fields.

Breaking Changes

Yes - The response format is a breaking change from positional arrays to named objects.

Acceptable because: This endpoint is being added in this PR and has not yet been released, so no existing consumers are affected.

Files Changed

  • src/main/java/com/dotcms/google/analytics/rest/GoogleAnalyticsResource.java - New REST resource with flattened response conversion
  • README.md - Added REST API documentation and dimensions/metrics explainer

🤖 Generated with Claude Code

fmontes and others added 2 commits February 16, 2026 14:44
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>
Improved REST API response format from positional arrays to self-documenting
named objects for better developer experience.

**Before:**
```json
{
  "rows": [
    {
      "dimensions": ["2026-02-09", "/products"],
      "metrics": ["150", "120"]
    }
  ]
}
```

**After:**
```json
{
  "dimensions": ["date", "pagePath"],
  "metrics": ["sessions", "activeUsers"],
  "rows": [
    {
      "date": "2026-02-09",
      "pagePath": "/products",
      "sessions": "150",
      "activeUsers": "120"
    }
  ]
}
```

**Changes:**
- Added dimension/metric name arrays to response metadata
- Flattened row structure with named fields instead of positional arrays
- Updated README with REST API usage examples
- Added dimensions & metrics explainer section

**Breaking change:** Response format changed, but acceptable since endpoint
not yet released.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a REST API endpoint for querying Google Analytics 4 data, enabling headless and API-first use cases beyond the existing Velocity template-based approach. The endpoint provides a self-documenting JSON response format with named fields rather than positional arrays.

Changes:

  • New POST endpoint at /api/v1/googleanalytics/query for programmatic GA4 data queries with flattened response format
  • Added JAX-RS dependency and OSGi bundle service registration
  • Documentation updates with REST API usage examples and dimensions/metrics explainer

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 14 comments.

File Description
src/main/java/com/dotcms/google/analytics/rest/GoogleAnalyticsResource.java New REST resource implementing the /query endpoint with authentication, request validation, and response transformation
src/main/java/com/dotcms/google/analytics/osgi/Activator.java Added REST resource registration via publishBundleServices call
build.gradle Added javax.ws.rs-api dependency and bumped version to 0.5.0
README.md Added REST API usage documentation with curl examples and dimensions/metrics educational content

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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")
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation mentions future date format. Line 150 shows an example date "20260209" which is in the future. While this is consistent with the API examples, consider clarifying that users should replace these with actual historical dates when testing, as Google Analytics typically only contains data for past dates.

Suggested change
- `date` - When the activity happened (e.g., "20260209")
- `date` - When the activity happened (e.g., "20240209" — use a past date that has data in your GA property when testing)

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +61
@POST
@Path("/query")
@Produces(MediaType.APPLICATION_JSON)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @consumes annotation for the POST endpoint. Add @consumes(MediaType.APPLICATION_JSON) to specify that this endpoint expects JSON input, which is important for proper content type handling and request parsing.

Copilot uses AI. Check for mistakes.
}

// Get analytics app configuration for current site
final String siteId = request.getServerName(); // Or extract from request
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect site/host identification method. Using request.getServerName() returns the hostname from the HTTP request (e.g., "localhost" or "example.com"), which is not the proper way to identify the dotCMS site. According to the pattern used in AnalyticsViewTool.java (lines 61-62), the correct approach is to use WebAPILocator.getHostWebAPI().getHost(request).getIdentifier() to get the site identifier. This is critical for multi-site setups where the same hostname might serve different content.

Copilot uses AI. Check for mistakes.
} catch (Exception e) {
Logger.error(this, "Error querying Google Analytics", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", e.getMessage()))
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential information disclosure in error handling. The error message returned in line 206 exposes the raw exception message which could leak sensitive information such as file paths, internal configuration details, or stack traces. Consider returning a generic error message to the client and logging the detailed error server-side for debugging purposes.

Suggested change
.entity(Map.of("error", e.getMessage()))
.entity(Map.of("error", "Error querying Google Analytics"))

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +86
"startDate": "2026-02-09",
"endDate": "2026-02-16",
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation contains future dates. The example dates "2026-02-09" and "2026-02-16" are in the future. While this may be intentional for the example, it could be confusing for users trying to test the API since Google Analytics typically only has data for past dates. Consider using relative date references (e.g., "7daysAgo", "today") or clearly indicating that these are example dates that should be replaced with actual historical dates.

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +138
// 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);
}
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation for filter operators. If an invalid operator value is provided in the filters, it will cause an IllegalArgumentException when Filter.StringFilter.MatchType.valueOf() is called in GoogleAnalyticsService.getFilterExpression() (line 152). This will be caught by the generic exception handler but will result in an unclear error message. Consider validating the operator values against the allowed MatchType enum values (EXACT, BEGINS_WITH, ENDS_WITH, CONTAINS, etc.) and returning a clear BAD_REQUEST response with a list of valid operators.

Copilot uses AI. Check for mistakes.
@Context final HttpServletResponse response,
final GoogleAnalyticsQueryRequest queryRequest) {

try {
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check for queryRequest parameter. The method should validate that queryRequest is not null before accessing its properties, otherwise a NullPointerException could occur. Add validation at the start of the method to return a BAD_REQUEST response if queryRequest is null.

Suggested change
try {
try {
// Validate request body
if (queryRequest == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "queryRequest is required"))
.build();
}

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +104
if (queryRequest.getStartDate() != null) {
analyticsRequest.setStartDate(queryRequest.getStartDate());
}
if (queryRequest.getEndDate() != null) {
analyticsRequest.setEndDate(queryRequest.getEndDate());
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation for date formats. The startDate and endDate fields are accepted as strings without validation. According to AnalyticsRequest.java (line 16), dates should follow the "YYYY-MM-dd" format. Invalid date formats could cause downstream errors in the Google Analytics API. Consider validating the date format and returning a clear error message if the format is invalid.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +113
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()));
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for empty lists. The code checks if metrics and dimensions are not null and not empty (lines 107, 112), but if the lists are provided but empty (e.g., "metrics": []), the validation passes but an empty string is passed to AnalyticsRequest.setMetrics() on line 108, which could lead to unexpected behavior. Consider validating that the lists contain at least one element or handle empty lists explicitly.

Suggested change
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()));
if (queryRequest.getMetrics() != null) {
final List<String> metrics = queryRequest.getMetrics()
.stream()
.filter(m -> m != null && !m.trim().isEmpty())
.collect(Collectors.toList());
if (!metrics.isEmpty()) {
analyticsRequest.setMetrics(String.join(",", metrics));
}
}
// Set dimensions
if (queryRequest.getDimensions() != null) {
final List<String> dimensions = queryRequest.getDimensions()
.stream()
.filter(d -> d != null && !d.trim().isEmpty())
.collect(Collectors.toList());
if (!dimensions.isEmpty()) {
analyticsRequest.setDimensions(String.join(",", dimensions));
}

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +148
if (queryRequest.getMaxResults() != null && queryRequest.getMaxResults() > 0) {
analyticsRequest.setMaxResults(queryRequest.getMaxResults());
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing upper bound validation for maxResults. While the code checks that maxResults is greater than 0 (line 147), there's no validation for an upper limit. Google Analytics API typically has limits on the number of results per request (often around 100,000). Allowing arbitrary large values could lead to performance issues or API errors. Consider adding a reasonable upper limit (e.g., 10,000) and returning a BAD_REQUEST error if exceeded.

Suggested change
if (queryRequest.getMaxResults() != null && queryRequest.getMaxResults() > 0) {
analyticsRequest.setMaxResults(queryRequest.getMaxResults());
if (queryRequest.getMaxResults() != null) {
final int MAX_RESULTS = 10_000;
final int maxResults = queryRequest.getMaxResults();
if (maxResults <= 0 || maxResults > MAX_RESULTS) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Invalid maxResults value. It must be between 1 and " + MAX_RESULTS + ".")
.build();
}
analyticsRequest.setMaxResults(maxResults);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +174 to +177
for (int i = 0; i < dimensionNames.size() && i < dimensionValues.size(); i++) {
rowData.put(dimensionNames.get(i), dimensionValues.get(i).getValue());
}
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop uses i < dimensionNames.size() && i < dimensionValues.size() to prevent index out of bounds, but this silently truncates data if there's a mismatch between the requested dimensions and the returned dimension values. If Google Analytics returns fewer dimension values than requested (which could indicate a problem), this would be silently ignored. Consider logging a warning when the sizes don't match, or returning an error to indicate incomplete data.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +108
{
"date": "20260209",
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example response shows date format as "20260209" (YYYYMMDD without separators) which differs from the input date format "2026-02-09" (YYYY-MM-DD with hyphens). This inconsistency could confuse API consumers. Consider documenting this format transformation clearly, explaining that Google Analytics returns dates in YYYYMMDD format regardless of how they're submitted in the request.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +114
// 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()));
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for the list fields (metrics, dimensions, filters). There's no check to ensure these lists are not empty when provided, or that they don't exceed Google Analytics API limits. For example, GA4 API limits requests to 10 metrics and 9 dimensions maximum. Without validation, invalid requests will fail at the Google Analytics API layer with less clear error messages.

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +197
metadata.put("currencyCode", gaResponse.getMetadata().getCurrencyCode());
metadata.put("timeZone", gaResponse.getMetadata().getTimeZone());
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullPointerException when gaResponse.getMetadata() returns null. If the Google Analytics API response doesn't include metadata (which could happen in error scenarios or with certain query types), this code will create an empty metadata map but won't check for null before calling getCurrencyCode() and getTimeZone(). While the current code has a null check on line 195, the subsequent calls on lines 196-197 don't guard against the metadata object itself being null. Consider adding proper null checks for the metadata fields themselves.

Suggested change
metadata.put("currencyCode", gaResponse.getMetadata().getCurrencyCode());
metadata.put("timeZone", gaResponse.getMetadata().getTimeZone());
final String currencyCode = gaResponse.getMetadata().getCurrencyCode();
if (currencyCode != null) {
metadata.put("currencyCode", currencyCode);
}
final String timeZone = gaResponse.getMetadata().getTimeZone();
if (timeZone != null) {
metadata.put("timeZone", timeZone);
}

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +83
.entity(Map.of("error", "propertyId is required"))
.build();
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error response uses Map.of() which creates an immutable map. While this works, it's inconsistent with the rest of the response structure which uses HashMap. Consider using a consistent approach throughout the error responses, or using a proper error response DTO to ensure consistency in error message format across the API.

Copilot uses AI. Check for mistakes.
Comment on lines +214 to +280
public static class GoogleAnalyticsQueryRequest {
private String propertyId;
private String startDate;
private String endDate;
private List<String> metrics;
private List<String> dimensions;
private FiltersDTO filters;
private String sort;
private Integer maxResults;

// Getters and setters
public String getPropertyId() { return propertyId; }
public void setPropertyId(String propertyId) { this.propertyId = propertyId; }

public String getStartDate() { return startDate; }
public void setStartDate(String startDate) { this.startDate = startDate; }

public String getEndDate() { return endDate; }
public void setEndDate(String endDate) { this.endDate = endDate; }

public List<String> getMetrics() { return metrics; }
public void setMetrics(List<String> metrics) { this.metrics = metrics; }

public List<String> getDimensions() { return dimensions; }
public void setDimensions(List<String> dimensions) { this.dimensions = dimensions; }

public FiltersDTO getFilters() { return filters; }
public void setFilters(FiltersDTO filters) { this.filters = filters; }

public String getSort() { return sort; }
public void setSort(String sort) { this.sort = sort; }

public Integer getMaxResults() { return maxResults; }
public void setMaxResults(Integer maxResults) { this.maxResults = maxResults; }
}

/**
* Filters container DTO.
*/
public static class FiltersDTO {
private List<FilterRequestDTO> dimension;
private List<FilterRequestDTO> metric;

public List<FilterRequestDTO> getDimension() { return dimension; }
public void setDimension(List<FilterRequestDTO> dimension) { this.dimension = dimension; }

public List<FilterRequestDTO> getMetric() { return metric; }
public void setMetric(List<FilterRequestDTO> metric) { this.metric = metric; }
}

/**
* Filter DTO.
*/
public static class FilterRequestDTO {
private String field;
private String value;
private String operator;

public String getField() { return field; }
public void setField(String field) { this.field = field; }

public String getValue() { return value; }
public void setValue(String value) { this.value = value; }

public String getOperator() { return operator; }
public void setOperator(String operator) { this.operator = operator; }
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inner DTOs (GoogleAnalyticsQueryRequest, FiltersDTO, FilterRequestDTO) lack any validation annotations or input sanitization. Fields like propertyId, field names, operator values, and filter values are accepted without validation. This could allow injection attacks or malformed data to reach the Google Analytics API. Consider adding validation annotations (e.g., @NotNull, @pattern, @SiZe) or explicit validation logic to ensure the input conforms to expected formats and constraints.

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +189
final List<Map<String, String>> rows = gaResponse.getRowsList().stream()
.map(row -> {
final Map<String, String> rowData = new HashMap<>();

// Map dimension values to names
final List<DimensionValue> dimensionValues = row.getDimensionValuesList();
if (dimensionNames != null) {
for (int i = 0; i < dimensionNames.size() && i < dimensionValues.size(); i++) {
rowData.put(dimensionNames.get(i), dimensionValues.get(i).getValue());
}
}

// Map metric values to names
final List<MetricValue> metricValues = row.getMetricValuesList();
if (metricNames != null) {
for (int i = 0; i < metricNames.size() && i < metricValues.size(); i++) {
rowData.put(metricNames.get(i), metricValues.get(i).getValue());
}
}

return rowData;
})
.collect(Collectors.toList());
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flattened response structure returns all metric and dimension values as strings, even though metrics are typically numeric values. This loses type information and could make client-side processing more difficult. Consumers will need to parse strings like "150" back to numbers. Consider preserving the numeric type for metrics in the response, or at minimum document this behavior clearly in the API response.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +62
@POST
@Path("/query")
@Produces(MediaType.APPLICATION_JSON)
public Response query(
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @consumes annotation on the POST endpoint. The endpoint expects JSON input (GoogleAnalyticsQueryRequest) but doesn't explicitly declare @consumes(MediaType.APPLICATION_JSON). While JAX-RS implementations may infer this, it's a best practice to explicitly declare the consumed media type to make the API contract clear and avoid potential deserialization issues.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +50
// Register REST resources
publishBundleServices(bundleContext);
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Activator calls publishBundleServices(bundleContext) to register REST resources, but the GoogleAnalyticsResource class is never explicitly passed to this method or registered with the OSGi container. The GenericBundleActivator.publishBundleServices() method typically expects resources to be registered, but there's no visible registration of the GoogleAnalyticsResource. This could mean the REST endpoint won't be accessible. Verify how REST resources should be registered in dotCMS OSGi plugins - you may need to add the resource to a collection or explicitly register it via the bundle context.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant