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

first pass, suppress daily question if user had no timeline #1769

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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 @@ -41,9 +41,12 @@ public class SleepStats {
public SleepStats(final Integer soundSleepDurationInMinutes,
final Integer mediumSleepDurationInMinutes,
final Integer lightSleepDurationInMinutes,
final Integer sleepDurationInMinutes, final boolean isInBedDuration,
final Integer sleepDurationInMinutes,
final boolean isInBedDuration,
final Integer numberOfMotionEvents,
final Long sleepTime, final Long wakeTime, final Integer sleepOnsetTimeMinutes) {
final Long sleepTime,
final Long wakeTime,
final Integer sleepOnsetTimeMinutes) {
this.soundSleepDurationInMinutes = soundSleepDurationInMinutes;
this.mediumSleepDurationInMinutes = mediumSleepDurationInMinutes;
this.lightSleepDurationInMinutes = lightSleepDurationInMinutes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.hello.suripu.core.db.QuestionResponseDAO;
import com.hello.suripu.core.db.SleepStatsDAODynamoDB;
import com.hello.suripu.core.db.util.MatcherPatternsDB;
import com.hello.suripu.core.db.TimeZoneHistoryDAODynamoDB;
import com.hello.suripu.core.models.AccountQuestion;
import com.hello.suripu.core.models.AccountQuestionResponses;
import com.hello.suripu.core.models.AggregateSleepStats;
import com.hello.suripu.core.models.Choice;
import com.hello.suripu.core.models.Question;
import com.hello.suripu.core.models.Response;
import com.hello.suripu.core.models.Questions.QuestionCategory;
import com.hello.suripu.core.models.TimeZoneHistory;
import com.hello.suripu.core.util.DateTimeUtil;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Days;
Expand Down Expand Up @@ -57,6 +60,7 @@ public class QuestionProcessor extends FeatureFlippedProcessor{

private final TimeZoneHistoryDAODynamoDB timeZoneHistoryDAODynamoDB;
private final QuestionResponseDAO questionResponseDAO;
private final SleepStatsDAODynamoDB sleepStatsDAODynamoDB;
private final int checkSkipsNum;

private final ListMultimap<Question.FREQUENCY, Integer> availableQuestionIds = ArrayListMultimap.create();
Expand All @@ -68,6 +72,7 @@ public class QuestionProcessor extends FeatureFlippedProcessor{
public static class Builder {
private QuestionResponseDAO questionResponseDAO;
private TimeZoneHistoryDAODynamoDB timeZoneHistoryDAODynamoDB;
private SleepStatsDAODynamoDB sleepStatsDAODynamoDB;
private int checkSkipsNum;
private ListMultimap<Question.FREQUENCY, Integer> availableQuestionIds;
private ListMultimap<Question.ASK_TIME, Integer> questionAskTimeMap;
Expand All @@ -85,6 +90,11 @@ public Builder withTimeZoneHistoryDaoDynamoDB(final TimeZoneHistoryDAODynamoDB t
return this;
}

public Builder withSleepStatsDAODynamoDB(final SleepStatsDAODynamoDB sleepStatsDAODynamoDB) {
this.sleepStatsDAODynamoDB = sleepStatsDAODynamoDB;
return this;
}

public Builder withCheckSkipsNum(final int checkSkipsNum) {
this.checkSkipsNum = checkSkipsNum;
return this;
Expand Down Expand Up @@ -128,20 +138,27 @@ public Builder withQuestions(final QuestionResponseDAO questionResponseDAO) {
}

public QuestionProcessor build() {
return new QuestionProcessor(this.questionResponseDAO, this.timeZoneHistoryDAODynamoDB, this.checkSkipsNum,
return new QuestionProcessor(this.questionResponseDAO,
this.timeZoneHistoryDAODynamoDB,
this.sleepStatsDAODynamoDB,
this.checkSkipsNum,
this.availableQuestionIds, this.questionAskTimeMap, this.questionIdMap, this.baseQuestionIds,
this.questionCategoryMap);
}
}

public QuestionProcessor(final QuestionResponseDAO questionResponseDAO, final TimeZoneHistoryDAODynamoDB timeZoneHistoryDAODynamoDB, final int checkSkipsNum,
public QuestionProcessor(final QuestionResponseDAO questionResponseDAO,
final TimeZoneHistoryDAODynamoDB timeZoneHistoryDAODynamoDB,
final SleepStatsDAODynamoDB sleepStatsDAODynamoDB,
final int checkSkipsNum,
final ListMultimap<Question.FREQUENCY, Integer> availableQuestionIds,
final ListMultimap<Question.ASK_TIME, Integer> questionAskTimeMap,
final Map<Integer, Question> questionIdMap,
final Set<Integer> baseQuestionIds,
final Map<QuestionCategory, List<Integer>> questionCategoryMap) {
this.questionResponseDAO = questionResponseDAO;
this.timeZoneHistoryDAODynamoDB = timeZoneHistoryDAODynamoDB;
this.sleepStatsDAODynamoDB = sleepStatsDAODynamoDB;
this.checkSkipsNum = checkSkipsNum;
this.availableQuestionIds.putAll(availableQuestionIds);
this.questionAskTimeMap.putAll(questionAskTimeMap);
Expand All @@ -152,37 +169,49 @@ public QuestionProcessor(final QuestionResponseDAO questionResponseDAO, final Ti
/**
* Get a list of questions for the user, or pre-generate one
*/
public List<Question> getQuestions(final Long accountId, final int accountAgeInDays, final DateTime today, final Integer numQuestions, final Boolean checkPause) {
public List<Question> getQuestions(final Long accountId, final int accountAgeInDays, final DateTime todayLocal, final Integer numQuestions, final Boolean checkPause) {

// brand new user - get on-boarding questions
if (accountAgeInDays < NEW_ACCOUNT_AGE) {
return this.getOnBoardingQuestions(accountId, today);
return this.getOnBoardingQuestions(accountId, todayLocal);
}

// check if user has skipped too many questions in the past.
if (checkPause) {
final boolean pauseQuestion = this.pauseQuestions(accountId, today);
final boolean pauseQuestion = this.pauseQuestions(accountId, todayLocal);
if (pauseQuestion) {
LOGGER.debug("Pause questions for user {}", accountId);
return Collections.emptyList();
}
} else {
this.resetNextAsk(accountId, today);
this.resetNextAsk(accountId, todayLocal);
}

// check if we have already generated a list of questions
// and if the user has answered any
final LinkedHashMap<Integer, Question> preGeneratedQuestions = Maps.newLinkedHashMap();

// grab user question and response status for today if this is not a "get-more questions" request
final DateTime expiration = today.plusDays(1);
final DateTime expiration = todayLocal.plusDays(1);
final ImmutableList<AccountQuestionResponses> questionResponseList = this.questionResponseDAO.getQuestionsResponsesByDate(accountId, expiration);

// check if we have generated any questions for this user TODAY
int answered = 0;
boolean foundAnomalyQuestion = false;
boolean hasCBTIGoals = hasCBTIGoalGoOutside(accountId);

// check if user has a valid timeline last night. Note: getTZOffset by date is not possible, circular logic
Boolean hasTimeline;
final Optional<Integer> timeZoneOffsetOptional = this.sleepStatsDAODynamoDB.getTimeZoneOffset(accountId);
if (!timeZoneOffsetOptional.isPresent()) {
hasTimeline = Boolean.FALSE;
} else {
final DateTime yesterdayLocal = todayLocal.minusDays(1);
final String yesterdayLocalString = DateTimeUtil.dateToYmdString(yesterdayLocal);
final Optional<AggregateSleepStats> sleepStatOptional = this.sleepStatsDAODynamoDB.getSingleStat(accountId, yesterdayLocalString);
hasTimeline = sleepStatOptional.isPresent();
}

if (!questionResponseList.isEmpty()) {
// check number of today's question the user has answered
for (final AccountQuestionResponses question : questionResponseList) {
Expand Down Expand Up @@ -224,7 +253,7 @@ public List<Question> getQuestions(final Long accountId, final int accountAgeInD
// question inserted into queue
preGeneratedQuestions.put(qid, Question.withAskTimeAccountQId(questionTemplate,
accountQId,
today,
todayLocal,
question.questionCreationDate));
}

Expand Down Expand Up @@ -252,9 +281,9 @@ public List<Question> getQuestions(final Long accountId, final int accountAgeInD
List<Question> questions;

if (accountAgeInDays < OLD_ACCOUNT_AGE) {
questions = this.getNewbieQuestions(accountId, today, getMoreNum, preGeneratedQuestions.keySet());
questions = this.getNewbieQuestions(accountId, todayLocal, getMoreNum, preGeneratedQuestions.keySet(), hasTimeline);
} else {
questions = this.getOldieQuestions(accountId, today, getMoreNum, preGeneratedQuestions.keySet());
questions = this.getOldieQuestions(accountId, todayLocal, getMoreNum, preGeneratedQuestions.keySet(), hasTimeline);
}

if (!preGeneratedQuestions.isEmpty()) {
Expand Down Expand Up @@ -418,7 +447,7 @@ private List<Question> getOnBoardingQuestions(Long accountId, DateTime today) {
/**
* Get questions for accounts less than 2 weeks old
*/
private List<Question> getNewbieQuestions(final Long accountId, final DateTime today, final Integer numQuestions, final Set<Integer> seenIds) {
private List<Question> getNewbieQuestions(final Long accountId, final DateTime today, final Integer numQuestions, final Set<Integer> seenIds, final Boolean hasTimeline) {

final List<Question> questions = new ArrayList<>();

Expand All @@ -428,15 +457,17 @@ private List<Question> getNewbieQuestions(final Long accountId, final DateTime t
// add questions that has already been selected
addedIds.addAll(seenIds);

// always include the ONE daily calibration question, most important Q has lower id
// choose ONE random daily-question IF there was a timeline generated last night. Most important Q has lower id
// This should always be question 22
final Integer questionId = this.availableQuestionIds.get(Question.FREQUENCY.DAILY).get(0);
if (!addedIds.contains(questionId)) {
addedIds.add(questionId);
final Long savedID = this.saveGeneratedQuestion(accountId, questionId, today);
if (savedID > 0L) {
final Question question = this.questionIdMap.get(questionId);
questions.add(Question.withAskTimeAccountQId(question, savedID, today, DateTime.now(DateTimeZone.UTC)));
if (hasTimeline) {
final Integer questionId = this.availableQuestionIds.get(Question.FREQUENCY.DAILY).get(0);
if (!addedIds.contains(questionId)) {
addedIds.add(questionId);
final Long savedID = this.saveGeneratedQuestion(accountId, questionId, today);
if (savedID > 0L) {
final Question question = this.questionIdMap.get(questionId);
questions.add(Question.withAskTimeAccountQId(question, savedID, today, DateTime.now(DateTimeZone.UTC)));
}
}
}

Expand Down Expand Up @@ -473,7 +504,7 @@ private List<Question> getNewbieQuestions(final Long accountId, final DateTime t
- take weekdays/weekends into account
- do not repeat base/ongoing questions within 2 ask days
*/
private List<Question> getOldieQuestions(final Long accountId, final DateTime today, final Integer numQuestions, final Set<Integer> seenIds) {
private List<Question> getOldieQuestions(final Long accountId, final DateTime today, final Integer numQuestions, final Set<Integer> seenIds, final Boolean hasTimeline) {

final List<Question> questions = new ArrayList<>();

Expand All @@ -483,14 +514,15 @@ private List<Question> getOldieQuestions(final Long accountId, final DateTime to
// add questions that has already been selected
addedIds.addAll(seenIds);

// always choose ONE random daily-question
List<Question> dailyQs = this.randomlySelectFromQuestionPool(accountId, seenIds, Question.FREQUENCY.DAILY, today, 1);
if (dailyQs.size() > 0 && !addedIds.contains(dailyQs.get(0).id)) {
addedIds.add(dailyQs.get(0).id);
questions.add(dailyQs.get(0));
// choose ONE random daily-question IF there was a timeline generated last night
if (hasTimeline) {
List<Question> dailyQs = this.randomlySelectFromQuestionPool(accountId, seenIds, Question.FREQUENCY.DAILY, today, 1);
if (dailyQs.size() > 0 && !addedIds.contains(dailyQs.get(0).id)) {
addedIds.add(dailyQs.get(0).id);
questions.add(dailyQs.get(0));
}
}


// first dib for base-question, randomly choose ONE
if (questions.size() < numQuestions) {
final Boolean answeredAll = addedIds.containsAll(this.baseQuestionIds);
Expand Down Expand Up @@ -520,6 +552,8 @@ private List<Question> randomlySelectFromQuestionPool(final long accountId, fina
final Question.FREQUENCY questionType,
final DateTime today, final int numQuestions) {

//Note: only daily question delivered is "how was your sleep last night" right now. Use of this method needs to change if we add other daily questions.

final List<Question> questions = new ArrayList<>();
List<Integer> eligibleQuestions = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@
import com.google.common.collect.Maps;
import com.hello.suripu.core.ObjectGraphRoot;
import com.hello.suripu.core.db.QuestionResponseDAO;
import com.hello.suripu.core.db.SleepStatsDAODynamoDB;
import com.hello.suripu.core.flipper.FeatureFlipper;
import com.hello.suripu.core.models.AccountInfo;
import com.hello.suripu.core.models.AccountQuestion;
import com.hello.suripu.core.models.AccountQuestionResponses;
import com.hello.suripu.core.models.AggregateSleepStats;
import com.hello.suripu.core.models.Choice;
import com.hello.suripu.core.models.MotionScore;
import com.hello.suripu.core.models.Question;
import com.hello.suripu.core.models.Questions.QuestionCategory;
import com.hello.suripu.core.models.Response;
import com.hello.suripu.core.models.SleepStats;
import com.hello.suripu.core.util.DateTimeUtil;
import com.librato.rollout.RolloutAdapter;
import com.librato.rollout.RolloutClient;
import dagger.Module;
Expand Down Expand Up @@ -118,6 +123,12 @@ public void setUp() {
ObjectGraphRoot.getInstance().init(new RolloutLocalModule());
features.clear();

final SleepStatsDAODynamoDB sleepStatsDAODynamoDB = mock(SleepStatsDAODynamoDB.class);
when(sleepStatsDAODynamoDB.getTimeZoneOffset(ACCOUNT_ID_PASS)).thenReturn(Optional.of(0));
when(sleepStatsDAODynamoDB.getSingleStat(ACCOUNT_ID_PASS, DateTimeUtil.dateToYmdString(this.today.minusDays(1)))).thenReturn(Optional.of(new AggregateSleepStats(0L, this.today, 0, 0, "String", new MotionScore(0,0,0F,0,0), 0, 0, 0, new SleepStats(0,0,0,0,Boolean.TRUE,0, 0L, 0L, 0))));

when(sleepStatsDAODynamoDB.getTimeZoneOffset(ACCOUNT_ID_FAIL)).thenReturn(Optional.of(0));
when(sleepStatsDAODynamoDB.getSingleStat(ACCOUNT_ID_FAIL, DateTimeUtil.dateToYmdString(this.today.minusDays(1)))).thenReturn(Optional.of(new AggregateSleepStats(0L, this.today, 0, 0, "String", new MotionScore(0,0,0F,0,0), 0, 0, 0, new SleepStats(0,0,0,0,Boolean.TRUE,0, 0L, 0L, 0))));

final List<Question> questions = this.getMockQuestions();
final QuestionResponseDAO questionResponseDAO = mock(QuestionResponseDAO.class);
Expand Down Expand Up @@ -176,6 +187,7 @@ public void setUp() {
final QuestionProcessor.Builder builder = new QuestionProcessor.Builder()
.withQuestionResponseDAO(questionResponseDAO)
.withCheckSkipsNum(CHECK_SKIP_NUM)
.withSleepStatsDAODynamoDB(sleepStatsDAODynamoDB)
.withQuestions(questionResponseDAO);

when(questionResponseDAO.getBaseAndRecentResponses(ACCOUNT_ID_PASS, Question.FREQUENCY.ONE_TIME.toSQLString(), oneWeekAgo))
Expand Down