diff --git a/ghost/members-events-service/lib/last-seen-at-updater.js b/ghost/members-events-service/lib/last-seen-at-updater.js index 54a5b314f0..ca3b0dc36b 100644 --- a/ghost/members-events-service/lib/last-seen-at-updater.js +++ b/ghost/members-events-service/lib/last-seen-at-updater.js @@ -1,6 +1,6 @@ const DomainEvents = require('@tryghost/domain-events'); const {MemberPageViewEvent} = require('@tryghost/member-events'); -const moment = require('moment'); +const moment = require('moment-timezone'); /** * Listen for `MemberViewEvent` to update the `member.last_seen_at` timestamp @@ -9,10 +9,21 @@ class LastSeenAtUpdater { /** * Initializes the event subscriber * @param {Object} deps dependencies - * @param {any} deps.memberModel The member model + * @param {Object} deps.models The list of model dependencies + * @param {any} deps.models.Member The Member model + * @param {Object} deps.services The list of service dependencies + * @param {any} deps.services.settingsCache The settings service */ - constructor({memberModel}) { - this._memberModel = memberModel; + constructor({ + models: { + Member + }, + services: { + settingsCache + } + }) { + this._memberModel = Member; + this._settingsCacheService = settingsCache; DomainEvents.subscribe(MemberPageViewEvent, async (event) => { await this.updateLastSeenAt(event.data.memberId, event.data.memberLastSeenAt, event.timestamp); }); @@ -28,7 +39,8 @@ class LastSeenAtUpdater { * @param {Date} timestamp The event timestamp */ async updateLastSeenAt(memberId, memberLastSeenAt, timestamp) { - if (memberLastSeenAt === null || moment(moment.utc(timestamp).startOf('day')).isAfter(moment.utc(memberLastSeenAt).startOf('day'))) { + const timezone = this._settingsCacheService.get('timezone'); + if (memberLastSeenAt === null || moment(moment.utc(timestamp).tz(timezone).startOf('day')).isAfter(moment.utc(memberLastSeenAt).tz(timezone).startOf('day'))) { await this._memberModel.update({ last_seen_at: moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss') }, { diff --git a/ghost/members-events-service/package.json b/ghost/members-events-service/package.json index 9ebf99b939..622618a2d3 100644 --- a/ghost/members-events-service/package.json +++ b/ghost/members-events-service/package.json @@ -27,6 +27,6 @@ "dependencies": { "@tryghost/domain-events": "^0.1.7", "@tryghost/member-events": "^0.3.5", - "moment": "^2.29.1" + "moment-timezone": "^0.5.34" } } diff --git a/ghost/members-events-service/test/last-seen-at-updater.test.js b/ghost/members-events-service/test/last-seen-at-updater.test.js index 3dd1248515..694ad8ed60 100644 --- a/ghost/members-events-service/test/last-seen-at-updater.test.js +++ b/ghost/members-events-service/test/last-seen-at-updater.test.js @@ -14,9 +14,63 @@ describe('LastSeenAtUpdater', function () { const now = moment('2022-02-28T18:00:00Z').utc(); const previousLastSeen = moment('2022-02-27T23:00:00Z').toISOString(); const spy = sinon.spy(); + const settingsCache = sinon.stub().returns('Etc/UTC'); new LastSeenAtUpdater({ - memberModel: { - update: spy + models: { + Member: { + update: spy + } + }, + services: { + settingsCache: { + get: settingsCache + } + } + }); + DomainEvents.dispatch(MemberPageViewEvent.create({memberId: '1', memberLastSeenAt: previousLastSeen, url: '/'}, now.toDate())); + assert(spy.calledOnceWithExactly({ + last_seen_at: now.format('YYYY-MM-DD HH:mm:ss') + }, { + id: '1' + }), 'The LastSeenAtUpdater should attempt a member update with the current date.'); + }); + + it('works correctly on another timezone (not updating last_seen_at)', async function () { + const now = moment('2022-02-28T04:00:00Z').utc(); + const previousLastSeen = moment('2022-02-27T20:00:00Z').toISOString(); + const spy = sinon.spy(); + const settingsCache = sinon.stub().returns('Asia/Bangkok'); + new LastSeenAtUpdater({ + models: { + Member: { + update: spy + } + }, + services: { + settingsCache: { + get: settingsCache + } + } + }); + DomainEvents.dispatch(MemberPageViewEvent.create({memberId: '1', memberLastSeenAt: previousLastSeen, url: '/'}, now.toDate())); + assert(spy.notCalled, 'The LastSeenAtUpdater should attempt a member update when the new timestamp is within the same day in the publication timezone.'); + }); + + it('works correctly on another timezone (updating last_seen_at)', async function () { + const now = moment('2022-02-28T04:00:00Z').utc(); + const previousLastSeen = moment('2022-02-27T20:00:00Z').toISOString(); + const spy = sinon.spy(); + const settingsCache = sinon.stub().returns('Europe/Paris'); + new LastSeenAtUpdater({ + models: { + Member: { + update: spy + } + }, + services: { + settingsCache: { + get: settingsCache + } } }); DomainEvents.dispatch(MemberPageViewEvent.create({memberId: '1', memberLastSeenAt: previousLastSeen, url: '/'}, now.toDate())); @@ -31,9 +85,17 @@ describe('LastSeenAtUpdater', function () { const now = moment('2022-02-28T18:00:00Z'); const previousLastSeen = moment('2022-02-28T00:00:00Z').toISOString(); const spy = sinon.spy(); + const settingsCache = sinon.stub().returns('Etc/UTC'); new LastSeenAtUpdater({ - memberModel: { - update: spy + models: { + Member: { + update: spy + } + }, + services: { + settingsCache: { + get: settingsCache + } } }); DomainEvents.dispatch(MemberPageViewEvent.create({memberId: '1', memberLastSeenAt: previousLastSeen, url: '/'}, now.toDate())); @@ -43,9 +105,17 @@ describe('LastSeenAtUpdater', function () { it('Doesn\'t fire on other events', async function () { const now = moment('2022-02-28T18:00:00Z'); const spy = sinon.spy(); + const settingsCache = sinon.stub().returns('Etc/UTC'); new LastSeenAtUpdater({ - memberModel: { - update: spy + models: { + Member: { + update: spy + } + }, + services: { + settingsCache: { + get: settingsCache + } } }); DomainEvents.dispatch(MemberSubscribeEvent.create({memberId: '1', source: 'api'}, now.toDate()));