Cleaned up old member analytics services

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

This change removes old analytics code which was added under `membersActivity` flag as an experimental alpha feature to test the first versions of member analytics, and is no longer active or in use.

This change removes the remaining services and its usage that were created to manage this version of analytics but is no longer active or maintained.

- removes `members-analytics-ingress` service that was used to ingest events from Portal in this experimental feature
- removes `member-analytics-service` service that managed the events from this experimental feature
- removes usages of the 2 services and their dependency in `members-api`
- removes `member-analytic-event` model as the corresponding table for it does not exist anymore and was dropped in 5.0
This commit is contained in:
Rishabh 2022-11-11 12:55:49 +05:30 committed by Rishabh Garg
parent d4c3f86ce0
commit d6af8fbb8f
24 changed files with 0 additions and 524 deletions

View File

@ -1,9 +0,0 @@
const ghostBookshelf = require('./base');
const MemberAnalyticEvent = ghostBookshelf.Model.extend({
tableName: 'temp_member_analytic_events'
});
module.exports = {
MemberAnalyticEvent: ghostBookshelf.model('MemberAnalyticEvent', MemberAnalyticEvent)
};

View File

@ -185,7 +185,6 @@ function createApiInstance(config) {
MemberPaymentEvent: models.MemberPaymentEvent,
MemberStatusEvent: models.MemberStatusEvent,
MemberProductEvent: models.MemberProductEvent,
MemberAnalyticEvent: models.MemberAnalyticEvent,
MemberCreatedEvent: models.MemberCreatedEvent,
SubscriptionCreatedEvent: models.SubscriptionCreatedEvent,
MemberLinkClickEvent: models.MemberClickEvent,

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/node'
]
};

View File

@ -1,19 +0,0 @@
const AnalyticEventRepository = require('./lib/AnalyticEventRepository');
const EventHandler = require('./lib/EventHandler');
class MemberAnalyticsService {
/**
* @param {AnalyticEventRepository} analyticEventRepository
*/
constructor(analyticEventRepository) {
this.eventHandler = new EventHandler(analyticEventRepository);
}
static create(AnalyticEventModel) {
const analyticEventRepository = new AnalyticEventRepository(AnalyticEventModel);
return new MemberAnalyticsService(analyticEventRepository);
}
}
module.exports = MemberAnalyticsService;

View File

@ -1,137 +0,0 @@
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const ObjectID = require('bson-objectid').default;
const messages = {
missingMemberId: 'A memberId must be provided for analytic events',
invalidEventName: 'Analytic events must be provided a "name"',
missingSourceUrl: 'A sourceUrl must be provided for analytic events',
invalidMemberStatus: 'A memberStatus of either "free", "paid" or "comped" must be provided'
};
/**
* @typedef {object} AnalyticEventProps
* @prop {ObjectID} id
* @prop {string} name
* @prop {Date} timestamp
* @prop {ObjectID} memberId
* @prop {'free'|'comped'|'paid'} memberStatus
* @prop {ObjectID | null} entryId
* @prop {string} sourceUrl
* @prop {string | null} metadata
*/
class AnalyticEvent {
get id() {
return this.props.id.toHexString();
}
get name() {
return this.props.name;
}
get timestamp() {
return this.props.timestamp;
}
get memberId() {
return this.props.memberId.toHexString();
}
get memberStatus() {
return this.props.memberStatus;
}
get entryId() {
return this.props.entryId.toHexString();
}
get sourceUrl() {
return this.props.sourceUrl;
}
get metadata() {
return this.props.metadata;
}
get isNew() {
return !!this.options.isNew;
}
/**
* @param {AnalyticEventProps} props
* @param {object} options
* @param {boolean} options.isNew
*/
constructor(props, options) {
this.props = props;
this.options = options;
}
/**
* @param {object} data
* @param {ObjectID | string} [data.id]
* @param {ObjectID | string} [data.entryId]
* @param {string} [data.metadata]
* @param {ObjectID | string} data.memberId
* @param {string} data.sourceUrl
* @param {string} data.name
* @param {string} data.memberStatus
* @param {Date} [data.timestamp]
*/
static create(data) {
let isNew = false;
let id;
if (data.id instanceof ObjectID) {
id = data.id;
} else if (typeof data.id === 'string') {
id = new ObjectID(data.id);
} else {
id = new ObjectID();
isNew = true;
}
let memberId;
if (data.memberId instanceof ObjectID) {
memberId = data.memberId;
} else if (typeof data.memberId === 'string') {
memberId = new ObjectID(data.memberId);
} else {
throw new errors.IncorrectUsageError({mesage: tpl(messages.missingMemberId)});
}
let entryId;
if (data.entryId instanceof ObjectID) {
entryId = data.entryId;
} else if (typeof data.entryId === 'string') {
entryId = new ObjectID(data.entryId);
} else {
entryId = null;
}
const name = data.name;
if (typeof name !== 'string') {
throw new errors.IncorrectUsageError({message: tpl(messages.invalidEventName)});
}
const timestamp = data.timestamp || new Date();
const sourceUrl = data.sourceUrl;
if (!sourceUrl) {
throw new errors.IncorrectUsageError({message: tpl(messages.missingSourceUrl)});
}
const memberStatus = data.memberStatus;
if (memberStatus !== 'free' && memberStatus !== 'paid' && memberStatus !== 'comped') {
throw new errors.IncorrectUsageError({message: tpl(messages.invalidMemberStatus)});
}
const metadata = data.metadata || null;
return new AnalyticEvent({
id,
name,
timestamp,
memberId,
memberStatus,
entryId,
sourceUrl,
metadata
}, {
isNew
});
}
}
module.exports = AnalyticEvent;

View File

@ -1,47 +0,0 @@
/**
* @typedef {object} DBProps
* @param {string} id
* @param {string} event_name
* @param {Date} created_at
* @param {string} member_id
* @param {string} member_status
* @param {string} entry_id
* @param {string} source_url
* @param {string} metadata
*/
class AnalyticEventRepository {
/**
* @param {any} AnalyticEventModel
*/
constructor(AnalyticEventModel) {
/** @private */
this.AnalyticEventModel = AnalyticEventModel;
}
/**
* @param {import('./AnalyticEvent')} event
*/
async save(event) {
const data = {
id: event.id,
event_name: event.name,
created_at: event.timestamp,
member_id: event.memberId,
member_status: event.memberStatus,
entry_id: event.entryId,
source_url: event.sourceUrl,
metadata: event.metadata
};
const model = this.AnalyticEventModel.forge(data);
if (event.isNew) {
await model.save(null, {method: 'insert'});
} else {
await model.save(null, {method: 'update'});
}
}
}
module.exports = AnalyticEventRepository;

View File

@ -1,94 +0,0 @@
const DomainEvents = require('@tryghost/domain-events');
const {
MemberEntryViewEvent,
MemberUnsubscribeEvent,
MemberSignupEvent,
MemberPaidConverstionEvent,
MemberPaidCancellationEvent
} = require('@tryghost/member-events');
const AnalyticEvent = require('./AnalyticEvent');
class EventHandler {
/**
* @param {import('./AnalyticEventRepository')} repository
*/
constructor(repository) {
/** @private */
this.repository = repository;
}
/**
* Listens for member events and handles creating analytic events and storing them.
*/
setupSubscribers() {
DomainEvents.subscribe(MemberEntryViewEvent, async (ev) => {
const event = AnalyticEvent.create({
name: 'entry_view',
memberId: ev.data.memberId,
memberStatus: ev.data.memberStatus,
entryId: ev.data.entryId,
sourceUrl: ev.data.entryUrl,
timestamp: ev.timestamp
});
await this.repository.save(event);
});
DomainEvents.subscribe(MemberUnsubscribeEvent, async (ev) => {
const event = AnalyticEvent.create({
name: 'unsubscribe',
memberId: ev.data.memberId,
memberStatus: ev.data.memberStatus,
entryId: ev.data.entryId,
sourceUrl: ev.data.sourceUrl,
timestamp: ev.timestamp
});
await this.repository.save(event);
});
DomainEvents.subscribe(MemberSignupEvent, async (ev) => {
const event = AnalyticEvent.create({
name: 'signup',
memberId: ev.data.memberId,
memberStatus: 'free',
entryId: ev.data.entryId,
sourceUrl: ev.data.sourceUrl,
timestamp: ev.timestamp
});
await this.repository.save(event);
});
DomainEvents.subscribe(MemberPaidCancellationEvent, async (ev) => {
const event = AnalyticEvent.create({
name: 'paid_cancellation',
memberId: ev.data.memberId,
memberStatus: ev.data.memberStatus,
entryId: ev.data.entryId,
sourceUrl: ev.data.sourceUrl,
metadata: ev.data.subscriptionId,
timestamp: ev.timestamp
});
await this.repository.save(event);
});
DomainEvents.subscribe(MemberPaidConverstionEvent, async (ev) => {
const event = AnalyticEvent.create({
name: 'paid_conversion',
memberId: ev.data.memberId,
memberStatus: ev.data.memberStatus,
entryId: ev.data.entryId,
sourceUrl: ev.data.sourceUrl,
metadata: ev.data.subscriptionId,
timestamp: ev.timestamp
});
await this.repository.save(event);
});
}
}
module.exports = EventHandler;

View File

@ -1,32 +0,0 @@
{
"name": "@tryghost/member-analytics-service",
"version": "0.0.0",
"private": true,
"author": "Ghost Foundation",
"license": "MIT",
"main": "index.js",
"scripts": {
"dev": "echo \"Implement me!\"",
"test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'",
"test": "yarn test:unit",
"lint": "eslint . --ext .js --cache"
},
"files": [
"index.js",
"lib"
],
"devDependencies": {
"@types/bookshelf": "1.2.7",
"c8": "7.12.0",
"mocha": "10.1.0",
"should": "13.2.3",
"sinon": "14.0.2"
},
"dependencies": {
"@tryghost/domain-events": "0.0.0",
"@tryghost/errors": "1.2.18",
"@tryghost/member-events": "0.0.0",
"@tryghost/tpl": "0.1.19",
"bson-objectid": "2.0.3"
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/test'
]
};

View File

@ -1,10 +0,0 @@
// Switch these lines once there are useful utils
// const testUtils = require('./utils');
require('./utils');
describe('Hello world', function () {
it('Runs a test', function () {
// TODO: Write me!
'hello'.should.eql('hello');
});
});

View File

@ -1,11 +0,0 @@
/**
* Custom Should Assertions
*
* Add any custom assertions to this file.
*/
// Example Assertion
// should.Assertion.add('ExampleAssertion', function () {
// this.params = {operator: 'to be a valid Example Assertion'};
// this.obj.should.be.an.Object;
// });

View File

@ -1,11 +0,0 @@
/**
* Test Utilities
*
* Shared utils for writing tests
*/
// Require overrides - these add globals for tests
require('./overrides');
// Require assertions - adds custom should assertions
require('./assertions');

View File

@ -1,10 +0,0 @@
// This file is required before any test is run
// Taken from the should wiki, this is how to make should global
// Should is a global in our eslint test config
global.should = require('should').noConflict();
should.extend();
// Sinon is a simple case
// Sinon is a global in our eslint test config
global.sinon = require('sinon');

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/node'
]
};

View File

@ -1 +0,0 @@
module.exports = require('./lib/EventsController');

View File

@ -1,36 +0,0 @@
const DomainEvents = require('@tryghost/domain-events');
const {MemberEntryViewEvent} = require('@tryghost/member-events');
/**
* @template Data
* @typedef {object} IEvent
* @prop {Date} timestamp
* @prop {Data} data
*/
class EventsController {
static createEvents(req, res) {
try {
const {events} = req.body;
for (const event of events) {
if (event.type === 'entry_view') {
const entryEvent = new MemberEntryViewEvent({
entryId: event.entry_id,
entryUrl: event.entry_url,
memberId: req.member ? req.member.id : null,
memberStatus: req.member ? req.member.status : null
}, event.created_at);
DomainEvents.dispatch(entryEvent);
}
}
res.writeHead(201);
return res.end('Created.');
} catch (err) {
const statusCode = (err && err.statusCode) || 500;
res.writeHead(statusCode);
return res.end('Internal Server Error.');
}
}
}
module.exports = EventsController;

View File

@ -1,28 +0,0 @@
{
"name": "@tryghost/members-analytics-ingress",
"version": "0.0.0",
"private": true,
"author": "Ghost Foundation",
"license": "MIT",
"main": "index.js",
"scripts": {
"dev": "echo \"Implement me!\"",
"test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'",
"test": "yarn test:unit",
"lint": "eslint . --ext .js --cache"
},
"files": [
"index.js",
"lib"
],
"devDependencies": {
"c8": "7.12.0",
"mocha": "10.1.0",
"should": "13.2.3",
"sinon": "14.0.2"
},
"dependencies": {
"@tryghost/domain-events": "0.0.0",
"@tryghost/member-events": "0.0.0"
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/test'
]
};

View File

@ -1,10 +0,0 @@
// Switch these lines once there are useful utils
// const testUtils = require('./utils');
require('./utils');
describe('Hello world', function () {
it('Runs a test', function () {
// TODO: Write me!
'hello'.should.eql('hello');
});
});

View File

@ -1,11 +0,0 @@
/**
* Custom Should Assertions
*
* Add any custom assertions to this file.
*/
// Example Assertion
// should.Assertion.add('ExampleAssertion', function () {
// this.params = {operator: 'to be a valid Example Assertion'};
// this.obj.should.be.an.Object;
// });

View File

@ -1,11 +0,0 @@
/**
* Test Utilities
*
* Shared utils for writing tests
*/
// Require overrides - these add globals for tests
require('./overrides');
// Require assertions - adds custom should assertions
require('./assertions');

View File

@ -1,10 +0,0 @@
// This file is required before any test is run
// Taken from the should wiki, this is how to make should global
// Should is a global in our eslint test config
global.should = require('should').noConflict();
should.extend();
// Sinon is a simple case
// Sinon is a global in our eslint test config
global.sinon = require('sinon');

View File

@ -4,8 +4,6 @@ const MagicLink = require('@tryghost/magic-link');
const errors = require('@tryghost/errors');
const logging = require('@tryghost/logging');
const MemberAnalyticsService = require('@tryghost/member-analytics-service');
const MembersAnalyticsIngress = require('@tryghost/members-analytics-ingress');
const PaymentsService = require('@tryghost/members-payments');
const TokenService = require('./services/token');
@ -48,7 +46,6 @@ module.exports = function MembersAPI({
MemberStatusEvent,
MemberProductEvent,
MemberEmailChangeEvent,
MemberAnalyticEvent,
MemberCreatedEvent,
SubscriptionCreatedEvent,
MemberLinkClickEvent,
@ -74,9 +71,6 @@ module.exports = function MembersAPI({
issuer
});
const memberAnalyticsService = MemberAnalyticsService.create(MemberAnalyticEvent);
memberAnalyticsService.eventHandler.setupSubscribers();
const productRepository = new ProductRepository({
Product,
Settings,
@ -322,10 +316,6 @@ module.exports = function MembersAPI({
body.json(),
forwardError((req, res) => routerController.createCheckoutSetupSession(req, res))
),
createEvents: Router().use(
body.json(),
forwardError((req, res) => MembersAnalyticsIngress.createEvents(req, res))
),
updateEmailAddress: Router().use(
body.json(),
forwardError((req, res) => memberController.updateEmailAddress(req, res))

View File

@ -32,9 +32,7 @@
"@tryghost/errors": "1.2.18",
"@tryghost/logging": "2.3.2",
"@tryghost/magic-link": "0.0.0",
"@tryghost/member-analytics-service": "0.0.0",
"@tryghost/member-events": "0.0.0",
"@tryghost/members-analytics-ingress": "0.0.0",
"@tryghost/members-payments": "0.0.0",
"@tryghost/nql": "0.11.0",
"@tryghost/tpl": "0.1.19",