Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(content analytics) fixes #30521 : Allow users to pass down simple Strings to query for Content Analytics data #30847

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class ContentAnalyticsQuery implements Serializable {
public static final String MEASURES_ATTR = "measures";
public static final String DIMENSIONS_ATTR = "dimensions";
public static final String TIME_DIMENSIONS_ATTR = "timeDimensions";
public static final String TIME_DIMENSIONS_DIMENSION_ATTR = "dimension";
public static final String FILTERS_ATTR = "filters";
public static final String ORDER_ATTR = "order";
public static final String LIMIT_ATTR = "limit";
Expand Down Expand Up @@ -166,7 +167,7 @@ public Builder timeDimensions(final String timeDimensions) {
}
final String[] timeParams = timeDimensions.split(SEPARATOR);
final Map<String, String> timeDimensionsData = new HashMap<>();
timeDimensionsData.put(DIMENSIONS_ATTR, timeParams[0]);
timeDimensionsData.put(TIME_DIMENSIONS_DIMENSION_ATTR, timeParams[0]);
if (timeParams.length > 2) {
timeDimensionsData.put(GRANULARITY_ATTR, timeParams[1]);
timeDimensionsData.put(DATE_RANGE_ATTR, timeParams[2]);
Expand Down Expand Up @@ -252,15 +253,36 @@ public Builder offset(final int offset) {
return this;
}


/**
* This method builds the ContentAnalyticsQuery object.
* This method builds the ContentAnalyticsQuery object based on all the specified
* parameters for the query.
*
* @return The ContentAnalyticsQuery object.
* @return The {@link ContentAnalyticsQuery} object.
*/
public ContentAnalyticsQuery build() {
return new ContentAnalyticsQuery(this);
}

/**
* This method builds the ContentAnalyticsQuery object based on all the specified
* parameters in the provided map.
*
* @param form A {@link Map} containing the query data.
*
* @return The {@link ContentAnalyticsQuery} object.
*/
public ContentAnalyticsQuery build(final Map<String, Object> form) {
this.measures((String) form.get(MEASURES_ATTR));
this.dimensions((String) form.get(DIMENSIONS_ATTR));
this.timeDimensions((String) form.get(TIME_DIMENSIONS_ATTR));
this.filters((String) form.get(FILTERS_ATTR));
this.order((String) form.get(ORDER_ATTR));
this.limit((Integer) form.get(LIMIT_ATTR));
this.offset((Integer) form.get(OFFSET_ATTR));
return new ContentAnalyticsQuery(this);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@
import com.dotmarketing.util.Config;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.UUIDUtil;
import com.dotmarketing.util.UtilMethods;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.annotations.VisibleForTesting;
import com.liferay.portal.model.User;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -44,7 +42,6 @@
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
Expand All @@ -55,13 +52,6 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static com.dotcms.analytics.content.ContentAnalyticsQuery.DIMENSIONS_ATTR;
import static com.dotcms.analytics.content.ContentAnalyticsQuery.FILTERS_ATTR;
import static com.dotcms.analytics.content.ContentAnalyticsQuery.LIMIT_ATTR;
import static com.dotcms.analytics.content.ContentAnalyticsQuery.MEASURES_ATTR;
import static com.dotcms.analytics.content.ContentAnalyticsQuery.OFFSET_ATTR;
import static com.dotcms.analytics.content.ContentAnalyticsQuery.ORDER_ATTR;
import static com.dotcms.analytics.content.ContentAnalyticsQuery.TIME_DIMENSIONS_ATTR;
import static com.dotcms.util.DotPreconditions.checkArgument;
import static com.dotcms.util.DotPreconditions.checkNotNull;

Expand Down Expand Up @@ -236,18 +226,20 @@ public ReportResponseEntityView queryCubeJs(@Context final HttpServletRequest re

/**
* Returns information of specific dotCMS objects whose health and engagement data is tracked,
* using Path Parameters instead of a CubeJS JSON query. This helps abstract the complexity of
* the underlying JSON format for users that need an easier way to query for specific data.
* using simplified version of a query sent to the Content Analytics service. This helps
* abstract the complexity of the underlying JSON format for users that need an easier way to
* query for specific data.
*
* @param request The current instance of the {@link HttpServletRequest} object.
* @param response The current instance of the {@link HttpServletResponse} object.
* @param params The query parameters provided in the URL path.
* @param form A Map with the parameters that will be used to generate the query
* internally.
*
* @return The request information from the Content Analytics server.
*/
@Operation(
operationId = "postContentAnalyticsQuery",
summary = "Retrieve Content Analytics data",
operationId = "postContentAnalyticsSimpleQuery",
summary = "Returns Content Analytics data",
description = "Returns information of specific dotCMS objects whose health and " +
"engagement data is tracked, using Path Parameters instead of a CubeJS JSON " +
"query. This helps abstract the complexity of the underlying JSON format for " +
Expand All @@ -259,17 +251,15 @@ public ReportResponseEntityView queryCubeJs(@Context final HttpServletRequest re
content = @Content(mediaType = "application/json",
examples = {
@ExampleObject(
value = "http://localhost:8080/api/v1" +
"/analytics/content/query/measures" +
"/request.count request" +
".totalSessions/dimensions/request" +
".host request.whatAmI request" +
".url/timeDimensions/request" +
".createdAt:day:This " +
"month/filters/request.totalRequest " +
"gt 0:request.whatAmI contains PAGE," +
"FILE/order/request.count asc:request" +
".createdAt asc/limit/5/offset/0"
value = "{\n" +
" \"measures\": \"request.count request.totalSessions\",\n" +
" \"dimensions\": \"request.host request.whatAmI request.url\",\n" +
" \"timeDimensions\": \"request.createdAt:day:Last month\",\n" +
" \"filters\": \"request.totalRequest gt 0:request.whatAmI contains PAGE,FILE\",\n" +
" \"order\": \"request.count asc:request.createdAt asc\",\n" +
" \"limit\": 15,\n" +
" \"offset\": 0\n" +
"}"
)
}
)
Expand All @@ -280,42 +270,25 @@ public ReportResponseEntityView queryCubeJs(@Context final HttpServletRequest re
}
)
@POST
@Path("/query/{params:.*}")
@JSONP
@NoCache
@Consumes(MediaType.APPLICATION_JSON)
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ReportResponseEntityView query(@Context final HttpServletRequest request,
@Context final HttpServletResponse response,
final @PathParam("params") String params) throws JsonProcessingException {
final Map<String, Object> form) {
final InitDataObject initDataObject = new WebResource.InitBuilder(this.webResource)
.requestAndResponse(request, response)
.params(params)
.requiredBackendUser(true)
.rejectWhenNoUser(true)
.init();
final User user = initDataObject.getUser();
final Map<String, String> paramsMap = initDataObject.getParamsMap();
Logger.debug(this, () -> "Querying content analytics data with the following parameters: " + paramsMap);
checkArgument(!(UtilMethods.isNotSet(paramsMap.get(MEASURES_ATTR))
&& UtilMethods.isNotSet(paramsMap.get(DIMENSIONS_ATTR))
&& UtilMethods.isNotSet(paramsMap.get(TIME_DIMENSIONS_ATTR.toLowerCase()))),
IllegalArgumentException.class, "Query should contain either measures, dimensions or timeDimensions with granularities in order to be valid");
final ContentAnalyticsQuery.Builder builder = new ContentAnalyticsQuery.Builder()
.measures(paramsMap.get(MEASURES_ATTR))
.dimensions(paramsMap.get(DIMENSIONS_ATTR))
.timeDimensions(paramsMap.get(TIME_DIMENSIONS_ATTR.toLowerCase()))
.filters(paramsMap.get(FILTERS_ATTR))
.order(paramsMap.get(ORDER_ATTR));
if (paramsMap.containsKey(LIMIT_ATTR)) {
builder.limit(Integer.parseInt(paramsMap.get(LIMIT_ATTR)));
}
if (paramsMap.containsKey(OFFSET_ATTR)) {
builder.offset(Integer.parseInt(paramsMap.get(OFFSET_ATTR)));
}
final ContentAnalyticsQuery contentAnalyticsQuery = builder.build();
Logger.debug(this, () -> "Querying content analytics data with the following parameters: " + form);
checkNotNull(form, IllegalArgumentException.class, "The 'form' JSON data cannot be null");
checkArgument(!form.isEmpty(), IllegalArgumentException.class, "The 'form' JSON data cannot be empty");
final ContentAnalyticsQuery contentAnalyticsQuery = new ContentAnalyticsQuery.Builder().build(form);
final String cubeJsQuery = JsonUtil.getJsonStringFromObject(contentAnalyticsQuery);
Logger.debug(this, ()-> "Generated query: " + cubeJsQuery);
Logger.debug(this, () -> "Generated query: " + cubeJsQuery);
final ReportResponse reportResponse = this.contentAnalyticsAPI.runRawReport(cubeJsQuery, user);
return new ReportResponseEntityView(reportResponse.getResults()
.stream().map(ResultSetItem::getAll).collect(Collectors.toList()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "10c05583-df4b-45e1-8794-0a1721e168f6",
"_postman_id": "630e7fe8-7824-4ab6-a56c-2f7c4056c596",
"name": "Content Analytics",
"description": "Performs simple data validation for the Content Analytics REST Endpoint. It's very important to notice that, for the time being, the CICD instance does not start up any of the additional third-party tools required to actually run the Content Analytics feature.\n\nThis means that these test do not deal with retrieveing or saving data at all. It verifies that important/required information is present.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
Expand All @@ -11,7 +11,7 @@
"name": "Data Query",
"item": [
{
"name": "Using Path Parameters",
"name": "Using Simple Strings",
"item": [
{
"name": "No User Authentication",
Expand All @@ -38,38 +38,23 @@
"header": [],
"body": {
"mode": "raw",
"raw": "",
"raw": "{\n \"measures\": \"request.count request.totalSessions\",\n \"dimensions\": \"request.host request.whatAmI request.url\",\n \"timeDimensions\": \"request.createdAt:day:Last month\",\n \"filters\": \"request.totalRequest gt 0:request.whatAmI contains PAGE,FILE\",\n \"order\": \"request.count asc:request.createdAt asc\",\n \"limit\": 5,\n \"offset\": 0\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{serverURL}}/api/v1/analytics/content/query/measures/request.count request.totalSessions/dimensions/request.host request.whatAmI request.url/timeDimensions/request.createdAt:day:This month/filters/request.totalRequest gt 0:request.whatAmI contains PAGE,FILE/order/request.count asc:request.createdAt asc/limit/5/offset/0",
"raw": "{{serverURL}}/api/v1/analytics/content",
"host": [
"{{serverURL}}"
],
"path": [
"api",
"v1",
"analytics",
"content",
"query",
"measures",
"request.count request.totalSessions",
"dimensions",
"request.host request.whatAmI request.url",
"timeDimensions",
"request.createdAt:day:This month",
"filters",
"request.totalRequest gt 0:request.whatAmI contains PAGE,FILE",
"order",
"request.count asc:request.createdAt asc",
"limit",
"5",
"offset",
"0"
"content"
]
}
},
Expand All @@ -87,7 +72,7 @@
"});",
"",
"pm.test(\"Check that minimum required parameters are NOT present\", function () {",
" pm.expect(pm.response.json().message).to.equal(\"Query should contain either measures, dimensions or timeDimensions with granularities in order to be valid\", \"This is NOT the expected error message\");",
" pm.expect(pm.response.json().message).to.equal(\"The 'form' JSON data cannot be null\");",
"});",
""
],
Expand Down Expand Up @@ -119,32 +104,23 @@
}
},
"url": {
"raw": "{{serverURL}}/api/v1/analytics/content/query/filters/request.totalRequest gt 0:request.whatAmI contains PAGE,FILE/order/request.count asc:request.createdAt asc/limit/5/offset/0",
"raw": "{{serverURL}}/api/v1/analytics/content",
"host": [
"{{serverURL}}"
],
"path": [
"api",
"v1",
"analytics",
"content",
"query",
"filters",
"request.totalRequest gt 0:request.whatAmI contains PAGE,FILE",
"order",
"request.count asc:request.createdAt asc",
"limit",
"5",
"offset",
"0"
"content"
]
},
"description": "As the error message states, the CubeJS Query should contain either measures, dimensions or timeDimensions with granularities in order to be valid."
},
"response": []
}
],
"description": "This test group verifies that the Endpoint that receives plain String parameters for the CubeJS query works as expected.\n\nParameter has specfic formatting and separator characters that allow Users a more dynamic interaction with the Content Analitycs service:\n\n- Measures: Values are separated by blank spaces: `/measures/request.count request.totalSessions`\n \n- Dimensions: Values are separated by blank spaces: `/dimensions/request.host request.whatAmI request.url`\n \n- Time Dimensions: Values are separated by colons: `/timeDimensions/request.createdAt:day:This month` . The second parameter 'day' -- the \"granularity\" parameter -- is optional.\n \n- Filters: Values are separated by colons: `/filters/request.totalRequest gt 0:request.whatAmI contains PAGE,FILE` . In this case, you're filtering by the number of requests, and the type of object being queried: Pages and Files.\n \n- Order: Values are separated by colon: `/order/request.count asc:request.createdAt asc`\n \n- Limit: Value is provided as is: `/limit/50`\n \n- Offset: Value is provided as is: `/ffset/0`"
"description": "This test group verifies that the Endpoint that receives simple String parameters for the Content Analytics query works as expected. This endpoint takes a JSON body with parameters such as the following:\n\n`{`\n\n`\"measures\": \"request.count request.totalSessions\",`\n\n`\"dimensions\": \"request.host request.whatAmI request.url\",`\n\n`\"timeDimensions\": \"request.createdAt:day:Last month\",`\n\n`\"filters\": \"request.totalRequest gt 0:request.whatAmI contains PAGE,FILE\",`\n\n`\"order\": \"request.count asc:request.createdAt asc\",`\n\n`\"limit\": 5,`\n\n`\"offset\": 0`\n\n`}`"
},
{
"name": "Using the JSON query",
Expand Down
Loading