First pass on adding domainevents to segment service

no issue

- In order to listen to `DomainEvents` for `MilestoneCreatedEvents` we need to add a `DomainEvents` listener and handler to the Segment analytics service.
- For better readability and to be more consistent with how code is currently written in Ghost, I refactored the service index file and split the two types of event listener into separate classes which is much cleaner and easier to test.
This commit is contained in:
Aileen Booker 2023-06-01 11:18:19 -04:00 committed by Aileen Booker
parent 20eaff9e5c
commit 2c4d9e2776
4 changed files with 134 additions and 39 deletions

View File

@ -0,0 +1,40 @@
const _ = require('lodash');
const logging = require('@tryghost/logging');
const DomainEvents = require('@tryghost/domain-events');
const {MilestoneCreatedEvent} = require('@tryghost/milestones');
module.exports = class DomainEventsAnalytics {
#analytics;
#trackDefaults;
#prefix;
#sentry;
constructor(deps) {
this.#analytics = deps.analytics;
this.#trackDefaults = deps.trackDefaults;
this.#prefix = deps.prefix;
this.#sentry = deps.sentry;
}
async #handleMilestoneCreatedEvent(type, event) {
if (type === MilestoneCreatedEvent
&& event.data.milestone
&& event.data.milestone.value === 100
) {
const eventName = event.data.milestone.type === 'arr' ? '$100 MRR reached' : '100 members reached';
try {
this.#analytics.track(_.extend(this.#trackDefaults, {}, {event: this.#prefix + eventName}));
} catch (err) {
logging.error(err);
this.#sentry.captureException(err);
}
}
}
subscribeToDomainEvents() {
DomainEvents.subscribe(MilestoneCreatedEvent, async (event) => {
await this.#handleMilestoneCreatedEvent(MilestoneCreatedEvent, event);
});
}
};

View File

@ -0,0 +1,63 @@
const _ = require('lodash');
const logging = require('@tryghost/logging');
// Listens to model events to layer on analytics - also uses the "fake" theme.uploaded event from the theme API
const events = require('../../lib/common/events');
const TO_TRACK = [
{
event: 'post.published',
name: 'Post Published'
},
{
event: 'page.published',
name: 'Page Published'
},
{
event: 'theme.uploaded',
name: 'Theme Uploaded',
// {keyOnSuppliedEventData: keyOnTrackedEventData}
// - used to extract specific properties from event data and give them meaningful names
data: {name: 'name'}
},
{
event: 'integration.added',
name: 'Custom Integration Added'
},
{
event: 'settings.edited',
name: 'Stripe enabled',
data: {key: 'key', value: 'value'}
}
];
module.exports = class ModelEventsAnalytics {
#analytics;
#trackDefaults;
#prefix;
#sentry;
#toTrack;
constructor(deps) {
this.#analytics = deps.analytics;
this.#trackDefaults = deps.trackDefaults;
this.#prefix = deps.prefix;
this.#sentry = deps.sentry;
this.#toTrack = TO_TRACK;
}
subscribeToModelEvents() {
this.#toTrack.forEach(({event, name, data = {}}) => {
events.on(event, function (eventData = {}) {
// extract desired properties from eventData and rename keys if necessary
const mappedData = _.mapValues(data || {}, v => eventData[v]);
try {
this.#analytics.track(_.extend(this.#trackDefaults, mappedData, {event: this.#prefix + name}));
} catch (err) {
logging.error(err);
this.#sentry.captureException(err);
}
});
});
}
};

View File

@ -1,51 +1,32 @@
const _ = require('lodash');
const Analytics = require('analytics-node');
const logging = require('@tryghost/logging');
const config = require('../../../shared/config');
const sentry = require('../../../shared/sentry');
// Listens to model events to layer on analytics - also uses the "fake" theme.uploaded event from the theme API
const events = require('../../lib/common/events');
const ModelEventsAnalytics = require('./ModelEventsAnalytics');
const DomainEventsAnalytics = require('./DomainEventsAnalytics');
module.exports.init = function () {
const analytics = new Analytics(config.get('segment:key'));
const trackDefaults = config.get('segment:trackDefaults') || {};
const prefix = config.get('segment:prefix') || '';
const toTrack = [
{
event: 'post.published',
name: 'Post Published'
},
{
event: 'page.published',
name: 'Page Published'
},
{
event: 'theme.uploaded',
name: 'Theme Uploaded',
// {keyOnSuppliedEventData: keyOnTrackedEventData}
// - used to extract specific properties from event data and give them meaningful names
data: {name: 'name'}
},
{
event: 'integration.added',
name: 'Custom Integration Added'
}
];
_.each(toTrack, function (track) {
events.on(track.event, function (eventData = {}) {
// extract desired properties from eventData and rename keys if necessary
const data = _.mapValues(track.data || {}, v => eventData[v]);
try {
analytics.track(_.extend(trackDefaults, data, {event: prefix + track.name}));
} catch (err) {
logging.error(err);
sentry.captureException(err);
}
});
const subscribeToDomainEvents = new DomainEventsAnalytics({
analytics,
trackDefaults,
prefix,
sentry
});
const modelEventsAnalytics = new ModelEventsAnalytics({
analytics,
trackDefaults,
prefix,
sentry
});
// Listen to model events
modelEventsAnalytics.subscribeToModelEvents();
// Listen to domain events
subscribeToDomainEvents.subscribeToDomainEvents();
};

View File

@ -0,0 +1,11 @@
const assert = require('assert');
describe('Segment Analytics Service', function () {
let segmentService;
it('Provides expected public API', async function () {
segmentService = require('../../../../../core/server/services/segment');
assert.ok(segmentService.initAndRun);
});
});