Track users activities on a Play! framework web app

Hi all -

Would like to track user activities on a Play 2.6 web app… have been searching in the document, could not find much. Basically, would like to be able to find the current logged in user and then use a filter to write to the access log we have.

Could someone please point me to the right document/example?

Thanks in advance!

Hi there,

Can you be more specific about what you call user activity ?
If you’re talking about which page they went on, that could be use with action composition

If you want to track what change they made in database, I’ve done such a thing using ebean in the past.

Ebean will try to load any class that implement ServerConfigStartup. So you can do something like that.

public class ActionLoggerConfigStartup implements ServerConfigStartup {
    @Override
    public void onStart(final ServerConfig serverConfig) {
        serverConfig.add(new ActionLoggerPersistController());
    }
}


public class ActionLoggerPersistController extends DefaultPersistController {
    @Override
    public int getExecutionOrder() {
        return 100000;
    }

    @Override
    public boolean isRegisterFor(final Class<?> cls) {
        return BaseModel.class.isAssignableFrom(cls);
    }

    @Override
    public void postInsert(final BeanPersistRequest<?> request) {
        ActionLoggerHelper.created(request);
    }

    @Override
    public boolean preUpdate(final BeanPersistRequest<?> request) {
        ActionLoggerHelper.modified(request);
        return true;
    }

    @Override
    public void preDelete(final BeanDeleteIdRequest request) {
        return;
    }

    @Override
    public void postDelete(final BeanPersistRequest<?> request) {
        ActionLoggerHelper.delete(request);
    }

    @Override
    public void postSoftDelete(final BeanPersistRequest<?> request) {
        ActionLoggerHelper.softDelete(request);
    }
}


public class ActionLoggerHelper {
    public final static Logger.ALogger LOGGER = Logger.of(ActionLoggerHelper.class);

    public static void created(final BeanPersistRequest<?> request) {
        executeWithContext(request, (account, organization, model) -> {
            final ActionLogModel actionLog = new ActionLogModel(
                account,
                organization,
                ActionLogModel.ActionType.CREATE,
                model
            );
            final EntityBean entityBean = (EntityBean) model;
            final List<ActionLogChangeEntity> changes = new ArrayList<>();
            for (int i = 0; i < entityBean._ebean_getPropertyNames().length; i++) {
                final Object o = entityBean._ebean_getField(i);
                changes.add(new ActionLogChangeEntity(
                    entityBean._ebean_getPropertyName(i),
                    null,
                    o));
            }
            actionLog.setChanges(changes);
            return actionLog;
        });
    }

    public static void modified(final BeanPersistRequest<?> request) {
        executeWithContext(request, (account, organization, model) -> {
            final ActionLogModel actionLog = new ActionLogModel(
                account,
                organization,
                ActionLogModel.ActionType.UPDATE,
                model
            );
            final EntityBean entityBean = (EntityBean) model;
            final List<ActionLogChangeEntity> changes = new ArrayList<>();
            entityBean._ebean_getIntercept().getDirtyValues().forEach((key, valuePair) -> {
                if ("lastUpdate".equals(key) || "dirty".equals(key)) {
                    return;
                }
                changes.add(new ActionLogChangeEntity(key, valuePair));
            });
            if (changes.size() == 0) {
                return null;
            }
            actionLog.setChanges(changes);
            return actionLog;
        });
    }

    public static void delete(final BeanPersistRequest<?> request) {
        executeWithContext(request, (account, organization, model) ->
            new ActionLogModel(account, organization, ActionLogModel.ActionType.DELETE, model)
        );
    }

    public static void softDelete(final BeanPersistRequest<?> request) {
        executeWithContext(request, (account, organization, model) ->
            new ActionLogModel(account, organization, ActionLogModel.ActionType.SOFT_DELETE, model)
        );
    }

    private static void executeWithContext(final BeanPersistRequest<?> request,
                                           final LoggerFunction function) {
        final AccountModel account = SessionHelper.getAccount();
        final OrganizationModel organization = SessionHelper.getOrganization();
        if (account == null || organization == null) {
            return;
        }

        final Object bean = request.getBean();
        if (!(bean instanceof BaseModel)) {
            LOGGER.warn("Bean is not an instance of BaseModel. Aborting.");
            return;
        }
        final BaseModel model = (BaseModel) bean;

        final PlayRedis playRedis = ApplicationService.application
            .injector()
            .instanceOf(PlayRedis.class);

        try (Jedis jedis = playRedis.getConnection()) {
            final ActionLogModel actionLog = function.apply(account, organization, model);
            if (actionLog != null) {
                jedis.rpush(RedisKeys.ACTION_LOGGER_FIFO, actionLog.getAsJson());
            }
        } catch (final Exception e) {
            LOGGER.error("An error occurred.", e);
        }
    }

    @FunctionalInterface
    private interface LoggerFunction {
        ActionLogModel apply(AccountModel account, OrganizationModel organization, BaseModel model);
    }
}

Please not that my models extends @MappedSuperclass BaseModel which itself extends Model. Allowing the logging process to only affect classes that extends BaseModel.

Please also not that my logging is backed by redis as a temporary cache before being flushed in my postgres database. The reason is if I remember correctly, that it avoid messing with Ebean life cycle as you’re in the middle of a transaction with an object being created, modified or deleted.

Hope that give you some insights as to what can be done.

Thank you very much for the reply - yes, what I was trying to do is mainly to check which page they went on and how long each page would take to finish. So the action composition did help us. Thank you very much for pointing that to us!

The changes to the database is not what we are tracking now, but we think that is also very useful to us, so we will also try that out. thanks for that as well. Will let you know if have more questions!

Thanks again!