0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Added the last_seen_at update on member page view

refs https://github.com/TryGhost/Team/issues/1306

- This adds a `MemberPageViewEvent` event when a page is viewed by a member (post/page/tag/author/...)
- Integrates the `LastSeenAtUpdater` service that listens to the `MemberPageViewEvent` events to update `member.last_seen_at`
- Follows the latest testing recommendation (end to end test + testing for side-effects)
This commit is contained in:
Thibaut Patel 2022-02-24 10:33:24 +01:00 committed by Thibaut Patel
parent 73a049c942
commit 527ef79955
6 changed files with 98 additions and 9 deletions

View file

@ -4,6 +4,8 @@ const express = require('../../shared/express');
const cors = require('cors');
const {URL} = require('url');
const errors = require('@tryghost/errors');
const DomainEvents = require('@tryghost/domain-events');
const {MemberPageViewEvent} = require('@tryghost/member-events');
// App requires
const config = require('../../shared/config');
@ -171,6 +173,14 @@ module.exports = function setupSiteApp(options = {}) {
}
});
siteApp.use(function (req, res, next) {
if (req.member) {
// This event needs memberLastSeenAt to avoid doing un-necessary database queries when updating `last_seen_at`
DomainEvents.dispatch(MemberPageViewEvent.create({url: req.url, memberId: req.member.id, memberLastSeenAt: req.member.last_seen_at}, new Date()));
}
next();
});
debug('General middleware done');
router = siteRoutes(options);

View file

@ -92,7 +92,7 @@ class GhostEventProcessor extends EventProcessor {
await this.db.knex('members')
.where('email', '=', event.recipientEmail)
.andWhere(builder => builder
.where('last_seen_at', '<', moment.utc(event.timestamp).tz(timezone).startOf('day').format('YYYY-MM-DD HH:mm:ss'))
.where('last_seen_at', '<', moment.utc(event.timestamp).tz(timezone).startOf('day').utc().format('YYYY-MM-DD HH:mm:ss'))
.orWhereNull('last_seen_at')
)
.update({

View file

@ -16,6 +16,8 @@ const models = require('../../models');
const {GhostMailer} = require('../mail');
const jobsService = require('../jobs');
const VerificationTrigger = require('@tryghost/verification-trigger');
const DomainEvents = require('@tryghost/domain-events');
const {LastSeenAtUpdater} = require('@tryghost/members-events-service');
const events = require('../../lib/common/events');
const messages = {
@ -139,7 +141,7 @@ module.exports = {
sendVerificationEmail: ({subject, message, amountImported}) => {
const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
const fromAddress = config.get('user_email');
if (escalationAddress) {
ghostMailer.send({
subject,
@ -158,6 +160,16 @@ module.exports = {
eventRepository: membersApi.events
});
new LastSeenAtUpdater({
models: {
Member: models.Member
},
services: {
domainEvents: DomainEvents,
settingsCache
}
});
(async () => {
try {
const collection = await models.SingleUseToken.fetchAll();

View file

@ -66,6 +66,7 @@
"@tryghost/custom-theme-settings-service": "0.3.1",
"@tryghost/database-info": "0.1.0",
"@tryghost/debug": "0.1.13",
"@tryghost/domain-events": "0.1.8",
"@tryghost/email-analytics-provider-mailgun": "1.0.7",
"@tryghost/email-analytics-service": "1.0.5",
"@tryghost/errors": "1.2.3",
@ -81,8 +82,9 @@
"@tryghost/limit-service": "1.0.9",
"@tryghost/logging": "2.0.4",
"@tryghost/magic-link": "1.0.19",
"@tryghost/member-events": "0.4.0",
"@tryghost/members-api": "5.0.4",
"@tryghost/members-events-service": "0.1.2",
"@tryghost/members-events-service": "0.3.1",
"@tryghost/members-importer": "0.5.2",
"@tryghost/members-offers": "0.10.7",
"@tryghost/members-ssr": "1.0.22",

View file

@ -1,3 +1,4 @@
const assert = require('assert');
const should = require('should');
const sinon = require('sinon');
const supertest = require('supertest');
@ -5,6 +6,9 @@ const moment = require('moment');
const testUtils = require('../utils');
const configUtils = require('../utils/configUtils');
const settingsCache = require('../../core/shared/settings-cache');
const DomainEvents = require('@tryghost/domain-events');
const {MemberPageViewEvent} = require('@tryghost/member-events');
const models = require('../../core/server/models');
function assertContentIsPresent(res) {
res.text.should.containEql('<h2 id="markdown">markdown</h2>');
@ -233,6 +237,18 @@ describe('Front-end members behaviour', function () {
.expect(200)
.expect(assertContentIsAbsent);
});
it('doesn\'t generate a MemberPageView event', async function () {
const spy = sinon.spy();
DomainEvents.subscribe(MemberPageViewEvent, spy);
await request
.get('/free-to-see/')
.expect(200)
.expect(assertContentIsPresent);
assert(spy.notCalled, 'A page view from a non-member shouldn\'t generate a MemberPageViewEvent event');
});
});
describe('as free member', function () {
@ -277,8 +293,9 @@ describe('Front-end members behaviour', function () {
});
describe('as free member with vip label', function () {
const email = 'vip@test.com';
before(async function () {
await loginAsMember('vip@test.com');
await loginAsMember(email);
});
it('can read label-only post content', async function () {
@ -287,15 +304,37 @@ describe('Front-end members behaviour', function () {
.expect(200)
.expect(assertContentIsPresent);
});
it('generates a MemberPageView event', async function () {
const spy = sinon.spy();
DomainEvents.subscribe(MemberPageViewEvent, spy);
// Reset last_seen_at property
let member = await models.Member.findOne({email});
await models.Member.edit({last_seen_at: null}, {id: member.get('id')});
member = await models.Member.findOne({email});
assert.equal(member.get('last_seen_at'), null, 'The member shouldn\'t have a `last_seen_at` property set before this test.');
await request
.get('/free-to-see/')
.expect(200)
.expect(assertContentIsPresent);
assert(spy.calledOnce, 'A page view from a member should generate a MemberPageViewEvent event');
member = await models.Member.findOne({email});
assert.notEqual(member.get('last_seen_at'), null, 'The member should have a `last_seen_at` property after having visited a page while logged-in.');
});
});
describe('as paid member', function () {
const email = 'paid@test.com';
before(async function () {
// membersService needs to be required after Ghost start so that settings
// are pre-populated with defaults
const membersService = require('../../core/server/services/members');
const signinLink = await membersService.api.getMagicLink('paid@test.com');
const signinLink = await membersService.api.getMagicLink(email);
const signinURL = new URL(signinLink);
// request needs a relative path rather than full url with host
const signinPath = `${signinURL.pathname}${signinURL.search}`;
@ -344,6 +383,27 @@ describe('Front-end members behaviour', function () {
.expect(200)
.expect(assertContentIsPresent);
});
it('generates a MemberPageView event', async function () {
const spy = sinon.spy();
DomainEvents.subscribe(MemberPageViewEvent, spy);
// Reset last_seen_at property
let member = await models.Member.findOne({email});
await models.Member.edit({last_seen_at: null}, {id: member.get('id')});
member = await models.Member.findOne({email});
assert.equal(member.get('last_seen_at'), null, 'The member shouldn\'t have a `last_seen_at` property set before this test.');
await request
.get('/free-to-see/')
.expect(200)
.expect(assertContentIsPresent);
assert(spy.calledOnce, 'A page view from a member should generate a MemberPageViewEvent event');
member = await models.Member.findOne({email});
assert.notEqual(member.get('last_seen_at'), null, 'The member should have a `last_seen_at` property after having visited a page while logged-in.');
});
});
describe('as paid member with vip label', function () {

View file

@ -1920,6 +1920,11 @@
"@tryghost/root-utils" "^0.3.10"
debug "^4.3.1"
"@tryghost/domain-events@0.1.8":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@tryghost/domain-events/-/domain-events-0.1.8.tgz#754af77b05336a689135971811ee438edda04876"
integrity sha512-PalAdGOADidoxXg54F/QJEc1C9PwlQvWnIF1MksWY2Pp8XiH5mzeHisNnqXfXO3dEEopPPreIj7Lx/y6U9NAyQ==
"@tryghost/domain-events@^0.1.7":
version "0.1.7"
resolved "https://registry.yarnpkg.com/@tryghost/domain-events/-/domain-events-0.1.7.tgz#dd6fa48886961c3e27889672a234bb516770d491"
@ -2252,10 +2257,10 @@
papaparse "5.3.1"
pump "^3.0.0"
"@tryghost/members-events-service@0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@tryghost/members-events-service/-/members-events-service-0.1.2.tgz#e8bc9dac1eca98f0cbe0372bc2460573b24f21bf"
integrity sha512-VbMAejI6daUiTyQCsA3FOhno7bPQNMWugbihAObwfyQayjoas8hYfrjk9/z9QMYh4N0A5JQugvXkL1gDQpbeLA==
"@tryghost/members-events-service@0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@tryghost/members-events-service/-/members-events-service-0.3.1.tgz#209758435bd6aebe46426a440e68d76541ce095e"
integrity sha512-bpjKYSzp1UjR6IPzzVLjoeSCbKT0rC7dr1SCwziavTD/2IuUA+ZKqxpPBeiGiQW+YFEvY6/NKXME+dr68fO6fw==
dependencies:
"@tryghost/domain-events" "0.1.8"
"@tryghost/member-events" "0.4.0"