Skip to content

Commit d6f3af1

Browse files
authored
Added structured logs for Ghost welcome emails enabled/disabled (#26339)
ref https://linear.app/ghost/issue/NY-1019/add-structured-logs-for-when-a-welcome-email-is-enableddisabled This adds two logging events when publishers enable or disable welcome emails: - welcome_email.enabled - a useful signal for feature adoption - welcome_email.disabled - a useful signal for feature (dis)-satisfaction This is also an initial experiment in structured logging in Ghost, which we can hopefully build on and convert most/all of Ghost's logs to a similar format over time to make them easier to search through. The reason for the "system" top level key is that this top level object is already ingested in Elastic search. We may want to adjust this at some point to something that makes more sense in the context of Ghost, but for now this should do the trick.
1 parent 61a2483 commit d6f3af1

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed

ghost/core/core/server/models/automated-email.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const ghostBookshelf = require('./base');
2+
const logging = require('@tryghost/logging');
23
const urlUtils = require('../../shared/url-utils');
34
const lexicalLib = require('../lib/lexical');
5+
const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../services/member-welcome-emails/constants');
6+
7+
const MEMBER_WELCOME_EMAIL_SLUG_SET = new Set(Object.values(MEMBER_WELCOME_EMAIL_SLUGS));
48

59
const AutomatedEmail = ghostBookshelf.Model.extend({
610
tableName: 'automated_emails',
@@ -33,6 +37,36 @@ const AutomatedEmail = ghostBookshelf.Model.extend({
3337
}
3438

3539
return attrs;
40+
},
41+
42+
onSaved(model) {
43+
if (!model?.id) {
44+
return;
45+
}
46+
47+
const slug = model.get('slug');
48+
49+
if (!MEMBER_WELCOME_EMAIL_SLUG_SET.has(slug)) {
50+
return;
51+
}
52+
53+
const previousStatus = model.previous('status');
54+
const currentStatus = model.get('status');
55+
const isNewModel = previousStatus === undefined;
56+
const isEnableTransition = currentStatus === 'active' && (isNewModel || previousStatus === 'inactive');
57+
const isDisableTransition = previousStatus === 'active' && currentStatus === 'inactive';
58+
59+
if (!isEnableTransition && !isDisableTransition) {
60+
return;
61+
}
62+
63+
logging.info({
64+
system: {
65+
event: isEnableTransition ? 'welcome_email.enabled' : 'welcome_email.disabled',
66+
automated_email_id: model.id,
67+
slug
68+
}
69+
}, isEnableTransition ? 'Welcome email enabled' : 'Welcome email disabled');
3670
}
3771
});
3872

ghost/core/test/e2e-api/admin/automated-emails.test.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const {agentProvider, fixtureManager, matchers, dbUtils} = require('../../utils/e2e-framework');
22
const {anyContentVersion, anyObjectId, anyISODateTime, anyErrorId, anyEtag, anyLocationFor} = matchers;
33
const sinon = require('sinon');
4+
const logging = require('@tryghost/logging');
45
const mailService = require('../../../core/server/services/mail');
56

67
const matchAutomatedEmail = {
@@ -169,6 +170,61 @@ describe('Automated Emails API', function () {
169170
etag: anyEtag
170171
});
171172
});
173+
174+
describe('Structured logging', function () {
175+
let infoStub;
176+
177+
beforeEach(function () {
178+
infoStub = sinon.stub(logging, 'info');
179+
});
180+
181+
afterEach(function () {
182+
sinon.restore();
183+
});
184+
185+
it('Logs when a welcome email is created as active', async function () {
186+
const {body} = await agent
187+
.post('automated_emails')
188+
.body({automated_emails: [{
189+
name: 'Welcome Email (Free)',
190+
slug: 'member-welcome-email-free',
191+
status: 'active',
192+
subject: 'Welcome to the site!',
193+
lexical: JSON.stringify({root: {children: []}})
194+
}]})
195+
.expectStatus(201);
196+
197+
const automatedEmail = body.automated_emails[0];
198+
199+
sinon.assert.calledWithMatch(infoStub, {
200+
system: {
201+
event: 'welcome_email.enabled',
202+
automated_email_id: automatedEmail.id,
203+
slug: 'member-welcome-email-free'
204+
}
205+
}, 'Welcome email enabled');
206+
});
207+
208+
it('Does not log when a welcome email is created as inactive', async function () {
209+
await agent
210+
.post('automated_emails')
211+
.body({automated_emails: [{
212+
name: 'Welcome Email (Free)',
213+
slug: 'member-welcome-email-free',
214+
status: 'inactive',
215+
subject: 'Welcome to the site!',
216+
lexical: JSON.stringify({root: {children: []}})
217+
}]})
218+
.expectStatus(201);
219+
220+
sinon.assert.neverCalledWithMatch(infoStub, {
221+
system: {
222+
event: 'welcome_email.enabled',
223+
slug: 'member-welcome-email-free'
224+
}
225+
}, sinon.match.any);
226+
});
227+
});
172228
});
173229

174230
describe('Edit', function () {
@@ -283,6 +339,76 @@ describe('Automated Emails API', function () {
283339
etag: anyEtag
284340
});
285341
});
342+
343+
describe('Structured logging', function () {
344+
let infoStub;
345+
346+
beforeEach(function () {
347+
infoStub = sinon.stub(logging, 'info');
348+
});
349+
350+
afterEach(function () {
351+
sinon.restore();
352+
});
353+
354+
it('Logs when a welcome email is enabled', async function () {
355+
const automatedEmail = await createAutomatedEmail({status: 'inactive'});
356+
357+
await agent
358+
.put(`automated_emails/${automatedEmail.id}`)
359+
.body({automated_emails: [{
360+
name: 'Welcome Email (Free)',
361+
status: 'active'
362+
}]})
363+
.expectStatus(200);
364+
365+
sinon.assert.calledWithMatch(infoStub, {
366+
system: {
367+
event: 'welcome_email.enabled',
368+
automated_email_id: automatedEmail.id,
369+
slug: 'member-welcome-email-free'
370+
}
371+
}, 'Welcome email enabled');
372+
});
373+
374+
it('Logs when a welcome email is disabled', async function () {
375+
const automatedEmail = await createAutomatedEmail({status: 'active'});
376+
377+
await agent
378+
.put(`automated_emails/${automatedEmail.id}`)
379+
.body({automated_emails: [{
380+
name: 'Welcome Email (Free)',
381+
status: 'inactive'
382+
}]})
383+
.expectStatus(200);
384+
385+
sinon.assert.calledWithMatch(infoStub, {
386+
system: {
387+
event: 'welcome_email.disabled',
388+
automated_email_id: automatedEmail.id,
389+
slug: 'member-welcome-email-free'
390+
}
391+
}, 'Welcome email disabled');
392+
});
393+
394+
it('Does not log when status does not change', async function () {
395+
const automatedEmail = await createAutomatedEmail({status: 'inactive'});
396+
397+
await agent
398+
.put(`automated_emails/${automatedEmail.id}`)
399+
.body({automated_emails: [{
400+
name: 'Welcome Email (Free)',
401+
subject: 'Updated subject only'
402+
}]})
403+
.expectStatus(200);
404+
405+
sinon.assert.neverCalledWithMatch(infoStub, {
406+
system: {
407+
event: sinon.match(/^welcome_email\.(enabled|disabled)$/)
408+
}
409+
}, sinon.match.any);
410+
});
411+
});
286412
});
287413

288414
describe('Destroy', function () {

0 commit comments

Comments
 (0)