diff --git a/ghost/core/core/server/services/segment/DomainEventsAnalytics.js b/ghost/core/core/server/services/segment/DomainEventsAnalytics.js new file mode 100644 index 0000000000..779a009a61 --- /dev/null +++ b/ghost/core/core/server/services/segment/DomainEventsAnalytics.js @@ -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); + }); + } +}; diff --git a/ghost/core/core/server/services/segment/ModelEventsAnalytics.js b/ghost/core/core/server/services/segment/ModelEventsAnalytics.js new file mode 100644 index 0000000000..f776fc511d --- /dev/null +++ b/ghost/core/core/server/services/segment/ModelEventsAnalytics.js @@ -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); + } + }); + }); + } +}; diff --git a/ghost/core/core/server/services/segment/index.js b/ghost/core/core/server/services/segment/index.js index 59e454243e..e9c6a20c71 100644 --- a/ghost/core/core/server/services/segment/index.js +++ b/ghost/core/core/server/services/segment/index.js @@ -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(); }; diff --git a/ghost/core/test/unit/server/services/segment/index.test.js b/ghost/core/test/unit/server/services/segment/index.test.js new file mode 100644 index 0000000000..dd049b28cb --- /dev/null +++ b/ghost/core/test/unit/server/services/segment/index.test.js @@ -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); + }); +});