/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rakam.analysis.funnel;
import com.fasterxml.jackson.annotation.JsonCreator;
import io.airlift.log.Logger;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.rakam.analysis.FunnelQueryExecutor;
import org.rakam.analysis.FunnelQueryExecutor.FunnelStep;
import org.rakam.analysis.FunnelQueryExecutor.FunnelWindow;
import org.rakam.analysis.QueryHttpService;
import org.rakam.report.QueryExecution;
import org.rakam.report.QueryResult;
import org.rakam.server.http.HttpService;
import org.rakam.server.http.RakamHttpRequest;
import org.rakam.server.http.annotations.Api;
import org.rakam.server.http.annotations.ApiOperation;
import org.rakam.server.http.annotations.ApiParam;
import org.rakam.server.http.annotations.Authorization;
import org.rakam.server.http.annotations.BodyParam;
import org.rakam.server.http.annotations.IgnoreApi;
import org.rakam.server.http.annotations.JsonRequest;
import org.rakam.util.JsonHelper;
import org.rakam.util.RakamException;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
@Path("/funnel")
@Api(value = "/funnel", nickname = "funnelAnalyzer", tags = "funnel")
public class FunnelAnalyzerHttpService
extends HttpService
{
private final FunnelQueryExecutor funnelQueryExecutor;
private final QueryHttpService queryService;
private final static Logger LOGGER = Logger.get(FunnelAnalyzerHttpService.class);
@Inject
public FunnelAnalyzerHttpService(FunnelQueryExecutor funnelQueryExecutor, QueryHttpService queryService)
{
this.funnelQueryExecutor = funnelQueryExecutor;
this.queryService = queryService;
}
@ApiOperation(value = "Execute query",
request = FunnelQuery.class,
consumes = "text/event-stream",
produces = "text/event-stream",
authorizations = @Authorization(value = "read_key")
)
@GET
@IgnoreApi
@Path("/analyze")
public void analyzeFunnel(RakamHttpRequest request)
{
queryService.handleServerSentQueryExecution(request, FunnelQuery.class, (project, query) -> {
QueryExecution execution = funnelQueryExecutor.query(project,
query.steps,
Optional.ofNullable(query.dimension),
query.startDate,
query.endDate,
Optional.ofNullable(query.window),
query.timezone,
Optional.ofNullable(query.connectors));
execution.getResult().thenAccept(data -> {
if (data.isFailed()) {
LOGGER.error(new RuntimeException(JsonHelper.encode(query) + " : " + data.getError().toString()), "Error running funnel query");
}
});
return execution;
});
}
@ApiOperation(value = "Execute query",
request = FunnelQuery.class,
authorizations = @Authorization(value = "read_key")
)
@POST
@JsonRequest
@Path("/analyze")
public CompletableFuture<QueryResult> analyzeFunnel(@Named("project") String project, @BodyParam FunnelQuery query)
{
CompletableFuture<QueryResult> result = funnelQueryExecutor.query(project,
query.steps,
Optional.ofNullable(query.dimension),
query.startDate,
query.endDate, Optional.ofNullable(query.window),
query.timezone, Optional.ofNullable(query.connectors)).getResult();
result.thenAccept(data -> {
if (data.isFailed()) {
LOGGER.error(new RuntimeException(JsonHelper.encode(query) + " : " + data.getError().toString()),
"Error running funnel query");
}
});
return result;
}
private static class FunnelQuery
{
public final List<FunnelStep> steps;
public final String dimension;
public final LocalDate startDate;
public final FunnelWindow window;
public final LocalDate endDate;
public final ZoneId timezone;
public final List<String> connectors;
@JsonCreator
public FunnelQuery(@ApiParam("steps") List<FunnelStep> steps,
@ApiParam(value = "dimension", required = false) String dimension,
@ApiParam("startDate") LocalDate startDate,
@ApiParam(value = "window", required = false) FunnelWindow window,
@ApiParam("endDate") LocalDate endDate,
@ApiParam("connectors") List<String> connectors,
@ApiParam(value = "timezone", required = false) String timezone)
{
this.steps = checkNotNull(steps, "steps field is required");
this.dimension = dimension;
this.startDate = startDate;
this.endDate = endDate;
this.connectors = connectors;
this.window = window;
try {
this.timezone = Optional.ofNullable(timezone)
.map(t -> ZoneId.of(t))
.orElse(ZoneOffset.UTC);
}
catch (Exception e) {
throw new RakamException("Timezone is invalid", HttpResponseStatus.BAD_REQUEST);
}
checkState(!steps.isEmpty(), "steps field cannot be empty.");
}
}
}