Analytics data point on action execution (#2740)

* Add analytics data point on action execution

* Include application details in action exec data point

* Only send action execution event on cloud

* Analytics is auto-disabled on self-hosted setups

* Move event name to AnalyticsEvents enum

* Move analytics Mono to separate method

* Use a common function to enqueue analytics message

* Provide analytics properties from caller method

* Use consistent casing in event names for analytics
This commit is contained in:
Shrikant Sharat Kandula 2021-02-02 20:24:27 +05:30 committed by GitHub
parent 11f5687b38
commit e9ba40f1f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 98 additions and 24 deletions

View File

@ -60,7 +60,7 @@ public class AuthenticationSuccessHandler implements ServerAuthenticationSuccess
.flatMap(user -> {
final boolean isFromInvite = user.getInviteToken() != null;
return Mono.whenDelayError(
analyticsService.sendEvent(AnalyticsEvents.FIRST_LOGIN, user, Map.of("isFromInvite", isFromInvite)),
analyticsService.sendObjectEvent(AnalyticsEvents.FIRST_LOGIN, user, Map.of("isFromInvite", isFromInvite)),
examplesOrganizationCloner.cloneExamplesOrganization()
);
})

View File

@ -1,13 +1,26 @@
package com.appsmith.server.constants;
import java.util.Locale;
public enum AnalyticsEvents {
CREATE,
UPDATE,
DELETE,
FIRST_LOGIN,
EXECUTE_ACTION("execute_ACTION_TRIGGERED"),
;
public String lowerName() {
return name().toLowerCase();
private final String eventName;
AnalyticsEvents() {
this.eventName = name().toLowerCase(Locale.ROOT);
}
AnalyticsEvents(String eventName) {
this.eventName = eventName;
}
public String getEventName() {
return eventName;
}
}

View File

@ -9,6 +9,7 @@ import com.segment.analytics.messages.TrackMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Mono;
import java.util.HashMap;
@ -27,8 +28,12 @@ public class AnalyticsService {
this.sessionUserService = sessionUserService;
}
public boolean isActive() {
return analytics != null;
}
public Mono<User> trackNewUser(User user) {
if (analytics == null) {
if (!isActive()) {
return Mono.just(user);
}
@ -51,16 +56,30 @@ public class AnalyticsService {
});
}
public <T extends BaseDomain> Mono<T> sendEvent(AnalyticsEvents event, T object) {
return sendEvent(event, object, null);
public void sendEvent(String event, String userId) {
sendEvent(event, userId, null);
}
public <T extends BaseDomain> Mono<T> sendEvent(AnalyticsEvents event, T object, Map<String, Object> extraProperties) {
if (analytics == null) {
public void sendEvent(String event, String userId, Map<String, Object> properties) {
if (!isActive()) {
return;
}
TrackMessage.Builder messageBuilder = TrackMessage.builder(event).userId(userId);
if (!CollectionUtils.isEmpty(properties)) {
messageBuilder = messageBuilder.properties(properties);
}
analytics.enqueue(messageBuilder);
}
public <T extends BaseDomain> Mono<T> sendObjectEvent(AnalyticsEvents event, T object, Map<String, Object> extraProperties) {
if (!isActive()) {
return Mono.just(object);
}
final String eventTag = event.lowerName() + "_" + object.getClass().getSimpleName().toUpperCase();
final String eventTag = event.getEventName() + "_" + object.getClass().getSimpleName().toUpperCase();
// We will create an anonymous user object for event tracking if no user is present
// Without this, a lot of flows meant for anonymous users will error out
@ -84,18 +103,13 @@ public class AnalyticsService {
analyticsProperties.putAll(extraProperties);
}
analytics.enqueue(
TrackMessage.builder(eventTag)
.userId(username)
.properties(analyticsProperties)
);
sendEvent(eventTag, username, analyticsProperties);
return object;
});
}
public <T extends BaseDomain> Mono<T> sendCreateEvent(T object, Map<String, Object> extraProperties) {
return sendEvent(AnalyticsEvents.CREATE, object, extraProperties);
return sendObjectEvent(AnalyticsEvents.CREATE, object, extraProperties);
}
public <T extends BaseDomain> Mono<T> sendCreateEvent(T object) {
@ -103,7 +117,7 @@ public class AnalyticsService {
}
public <T extends BaseDomain> Mono<T> sendUpdateEvent(T object, Map<String, Object> extraProperties) {
return sendEvent(AnalyticsEvents.UPDATE, object, extraProperties);
return sendObjectEvent(AnalyticsEvents.UPDATE, object, extraProperties);
}
public <T extends BaseDomain> Mono<T> sendUpdateEvent(T object) {
@ -111,10 +125,11 @@ public class AnalyticsService {
}
public <T extends BaseDomain> Mono<T> sendDeleteEvent(T object, Map<String, Object> extraProperties) {
return sendEvent(AnalyticsEvents.DELETE, object, extraProperties);
return sendObjectEvent(AnalyticsEvents.DELETE, object, extraProperties);
}
public <T extends BaseDomain> Mono<T> sendDeleteEvent(T object) {
return sendDeleteEvent(object, null);
}
}

View File

@ -15,15 +15,18 @@ import com.appsmith.external.pluginExceptions.StaleConnectionException;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.acl.PolicyGenerator;
import com.appsmith.server.constants.AnalyticsEvents;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.ActionProvider;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Page;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.dtos.ActionViewDTO;
import com.appsmith.server.dtos.ExecuteActionDTO;
@ -82,6 +85,8 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
private final MarketplaceService marketplaceService;
private final PolicyGenerator policyGenerator;
private final NewPageService newPageService;
private final ApplicationService applicationService;
private final SessionUserService sessionUserService;
public NewActionServiceImpl(Scheduler scheduler,
Validator validator,
@ -95,7 +100,9 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
PluginExecutorHelper pluginExecutorHelper,
MarketplaceService marketplaceService,
PolicyGenerator policyGenerator,
NewPageService newPageService) {
NewPageService newPageService,
ApplicationService applicationService,
SessionUserService sessionUserService) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.repository = repository;
this.datasourceService = datasourceService;
@ -105,6 +112,8 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
this.marketplaceService = marketplaceService;
this.policyGenerator = policyGenerator;
this.newPageService = newPageService;
this.applicationService = applicationService;
this.sessionUserService = sessionUserService;
}
private Boolean validateActionName(String name) {
@ -410,7 +419,7 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
}
// The client does not know about this field. Hence the default value takes over. Set this to null to ensure
// the update doesn't lead to resetting of this field.
// the update doesn't lead to resetting of this field.
action.setUserSetOnLoad(null);
Mono<NewAction> updatedActionMono = repository.findById(id, MANAGE_ACTIONS)
@ -456,8 +465,11 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
// Initialize the name to be empty value
actionName.set("");
// 2. Fetch the action from the DB and check if it can be executed
Mono<ActionDTO> actionMono = repository.findById(actionId, EXECUTE_ACTIONS)
Mono<NewAction> actionMono = repository.findById(actionId, EXECUTE_ACTIONS)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId)))
.cache();
Mono<ActionDTO> actionDTOMono = actionMono
.flatMap(dbAction -> {
ActionDTO action;
if (TRUE.equals(executeActionDTO.getViewMode())) {
@ -491,7 +503,7 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
// 3. Instantiate the implementation class based on the query type
Mono<Datasource> datasourceMono = actionMono
Mono<Datasource> datasourceMono = actionDTOMono
.flatMap(action -> {
// Global datasource requires us to fetch the datasource from DB.
if (action.getDatasource() != null && action.getDatasource().getId() != null) {
@ -533,7 +545,7 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
// 4. Execute the query
Mono<ActionExecutionResult> actionExecutionResultMono = Mono
.zip(
actionMono,
actionDTOMono,
datasourceMono,
pluginExecutorMono
)
@ -568,7 +580,9 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
)
);
return executionMono
return Mono.zip(actionMono, actionDTOMono)
.flatMap(tuple1 -> getAnalyticsMono(tuple1.getT1(), tuple1.getT2(), executeActionDTO))
.then(executionMono)
.onErrorResume(StaleConnectionException.class, error -> {
log.info("Looks like the connection is stale. Retrying with a fresh context.");
return datasourceContextService
@ -625,6 +639,38 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
});
}
private Mono<Void> getAnalyticsMono(NewAction action, ActionDTO actionDTO, ExecuteActionDTO executeActionDTO) {
// Since we're loading the application from DB *only* for analytics, we check if analytics is
// active before making the call to DB.
if (!analyticsService.isActive()) {
return Mono.empty();
}
return Mono.justOrEmpty(action.getApplicationId())
.flatMap(applicationService::findById)
.defaultIfEmpty(new Application())
.zipWith(sessionUserService.getCurrentUser())
.map(tuple -> {
final Application application = tuple.getT1();
final User user = tuple.getT2();
analyticsService.sendEvent(
AnalyticsEvents.EXECUTE_ACTION.getEventName(),
user.getUsername(),
Map.of(
"type", action.getPluginType(),
"name", actionDTO.getName(),
"pageId", actionDTO.getPageId(),
"appId", action.getApplicationId(),
"appMode", Boolean.TRUE.equals(executeActionDTO.getViewMode()) ? "view" : "edit",
"appName", application.getName(),
"isExampleApp", application.isAppIsExample()
)
);
return user;
})
.then();
}
private void prepareConfigurationsForExecution(ActionDTO action,
Datasource datasource,
ExecuteActionDTO executeActionDTO,