n.type === 'DROP_CAP_REACHED');
+ if (prevNotifIndex !== -1) user.notifications.splice(prevNotifIndex, 1);
+
+ user.addNotification('DROP_CAP_REACHED', {
+ message: i18n.t('dropCapReached', req.language),
+ items: dropN,
+ });
+ }
+
+ if (isEnrolledInDropCapTest) {
+ analytics.track('drop cap reached', {
+ uuid: user._id,
+ dropCap: maxDropCount,
+ category: 'behavior',
+ headers: req.headers,
+ });
+ }
+
if (analytics && moment().diff(user.auth.timestamps.created, 'days') < 7) {
analytics.track('dropped item', {
uuid: user._id,
@@ -164,15 +196,6 @@ export default function randomDrop (user, options, req = {}, analytics) {
category: 'behavior',
headers: req.headers,
});
-
- if (user.items.lastDrop.count === maxDropCount) {
- analytics.track('drop cap reached', {
- uuid: user._id,
- dropCap: maxDropCount,
- category: 'behavior',
- headers: req.headers,
- });
- }
}
}
}
diff --git a/website/server/libs/taskManager.js b/website/server/libs/taskManager.js
index a5d0f0ac88..f63db73a83 100644
--- a/website/server/libs/taskManager.js
+++ b/website/server/libs/taskManager.js
@@ -464,8 +464,12 @@ async function scoreTask (user, task, direction, req, res) {
user,
});
+ const isEnrolledInDropCapTest = user._ABtests.dropCapNotif
+ && user._ABtests.dropCapNotif !== 'drop-cap-notif-not-enrolled';
+
// Track when new users (first 7 days) score tasks
- if (moment().diff(user.auth.timestamps.created, 'days') < 7) {
+ // or if they're enrolled in the Drop Cap A/B Test
+ if (moment().diff(user.auth.timestamps.created, 'days') < 7 || isEnrolledInDropCapTest) {
res.analytics.track('task score', {
uuid: user._id,
hitType: 'event',
diff --git a/website/server/middlewares/cron.js b/website/server/middlewares/cron.js
index 62e1e85a68..ce004a2f68 100644
--- a/website/server/middlewares/cron.js
+++ b/website/server/middlewares/cron.js
@@ -52,6 +52,26 @@ async function unlockUser (user) {
}).exec();
}
+// Enroll users in the Drop Cap A/B Test
+function dropCapABTest (user, req) {
+ // Only target users that use web for cron and aren't subscribed.
+ // Those using mobile aren't excluded as they may use it later
+ const isWeb = req.headers['x-client'] === 'habitica-web';
+
+ if (isWeb && !user._ABtests.dropCapNotif && !user.isSubscribed()) {
+ const testGroup = Math.random();
+ // Enroll 20% of users, splitting them 50/50
+ if (testGroup <= 0.1) {
+ user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
+ } else if (testGroup <= 0.2) {
+ user._ABtests.dropCapNotif = 'drop-cap-notif-disabled';
+ } else {
+ user._ABtests.dropCapNotif = 'drop-cap-notif-not-enrolled';
+ }
+ user.markModified('_ABtests');
+ }
+}
+
async function cronAsync (req, res) {
let { user } = res.locals;
if (!user) return null; // User might not be available when authentication is not mandatory
@@ -66,6 +86,7 @@ async function cronAsync (req, res) {
res.locals.user = user;
const { daysMissed, timezoneUtcOffsetFromUserPrefs } = user.daysUserHasMissed(now, req);
+ dropCapABTest(user, req);
await updateLastCron(user, now);
if (daysMissed <= 0) {
diff --git a/website/server/models/userNotification.js b/website/server/models/userNotification.js
index d9cd9970bf..3425a5c9d3 100644
--- a/website/server/models/userNotification.js
+++ b/website/server/models/userNotification.js
@@ -60,6 +60,7 @@ const NOTIFICATION_TYPES = [
'ACHIEVEMENT_GOOD_AS_GOLD',
'ACHIEVEMENT_ALL_THAT_GLITTERS',
'ACHIEVEMENT', // generic achievement notification, details inside `notification.data`
+ 'DROP_CAP_REACHED',
];
const { Schema } = mongoose;