Added page attribution to member email alerts (#16381)

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

- adds attribution title and url for new free members and paid subscriptions to email alert

---------

Co-authored-by: Peter Zimon <peter.zimon@gmail.com>
This commit is contained in:
Rishabh Garg 2023-03-10 20:14:53 +05:30 committed by GitHub
parent 89493893d1
commit c71582877c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 34 deletions

View File

@ -12,6 +12,7 @@ class StaffServiceWrapper {
const logging = require('@tryghost/logging');
const models = require('../../models');
const memberAttribution = require('../member-attribution');
const {GhostMailer} = require('../mail');
const mailer = new GhostMailer();
const settingsCache = require('../../../shared/settings-cache');
@ -26,6 +27,7 @@ class StaffServiceWrapper {
settingsCache,
urlUtils,
DomainEvents,
memberAttributionService: memberAttribution.service,
labs
});

View File

@ -37,21 +37,18 @@
<td align="left" style="padding: 16px;">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td style="padding-right: 14px; background-color: #F9F9FA; text-align: left; vertical-align: top;" valign="top">
<td style="padding-right: 14px; background-color: #F9F9FA; text-align: left; vertical-align: middle;" valign="middle">
<div style="width: 48px; height: 48px; background-color: #15171A; border-radius: 999px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 19px; color: #FFFFFF; text-align: center; vertical-align: center; font-weight: 500; line-height: 47px;">
{{memberData.initials}}
</div>
</td>
<td style="padding-right: 8px; background-color: #F9F9FA; text-align: left; vertical-align: top;" valign="top">
<td style="padding-right: 8px; background-color: #F9F9FA; text-align: left; vertical-align: middle;" valign="middle">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{memberData.name}}</p>
{{#if memberData.showEmail}}
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #394047; font-weight: 400;">{{memberData.email}}</p>
{{/if}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Created on {{memberData.createdAt}}{{#if memberData.location}} &#8226; {{memberData.location}} {{/if}}
</p>
{{#if referrerSource}}
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Source: {{referrerSource}}</p>
{{/if}}
</td>
</tr>
</table>
@ -60,6 +57,25 @@
</tbody>
</table>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tr>
<td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-top: 32px; padding-bottom: 12px;">
{{#if referrerSource}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Signup info</p>
<hr style="border-bottom: 1px solid #F4F4F5; margin-top: 4px; margin-bottom: 8px;" />
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600; padding-bottom: 4px;">Source
<span style="font-weight: normal; color:#3A464C;"> - {{referrerSource}}</span>
</p>
{{#if attributionTitle}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">Page
<span style="font-weight: normal; color:#3A464C;"> - <a href="{{attributionUrl}}" style="font-weight: normal; color:#3A464C;text-decoration:none">{{attributionTitle}}</a></span>
</p>
{{/if}}
{{/if}}
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>

View File

@ -37,45 +37,58 @@
<td align="left" style="padding: 16px;">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td style="padding-right: 14px; vertical-align: top;" valign="top">
<td style="padding-right: 14px; vertical-align: middle;" valign="middle">
<div style="width: 48px; height: 48px; background-color: #15171A; border-radius: 999px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 19px; color: #FFFFFF; text-align: center; vertical-align: center; font-weight: 500; line-height: 47px;">
{{memberData.initials}}
</div>
</td>
<td style="padding-right: 8px; vertical-align: top;" valign="top">
<td style="padding-right: 8px; vertical-align: middle;" valign="middle">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{memberData.name}}</p>
{{#if memberData.showEmail}}
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #394047; font-weight: 400;">{{memberData.email}}</p>
{{/if}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Subscription started on {{subscriptionData.startedOn}} </p>
{{#if referrerSource}}
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Source: {{referrerSource}}</p>
{{/if}}
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="vertical-align: top; padding-top: 0; padding-right: 16px; padding-bottom: 16px; padding-left: 16px;">
<hr style="border-bottom: 2px solid #F4F4F5; margin-top: 0; margin-bottom: 16px;" />
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Tier</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{tierData.name}}
{{#if tierData.details}} <span style="font-weight: normal; color:#3A464C;"> - {{tierData.details}}</span>{{/if}}
</p>
</td>
</tr>
{{#if offerData}}
<tr>
<td style="vertical-align: top; padding-top: 0; padding-right: 16px; padding-bottom: 16px; padding-left: 16px;">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Offer</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{offerData.name}} <span style="font-weight: normal; color:#3A464C;"> - {{offerData.details}}</span></p>
</td>
</tr>
{{/if}}
</tbody>
</table>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tr>
<td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-top: 32px; padding-bottom: 12px;">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Tier</p>
<hr style="border-bottom: 1px solid #F4F4F5; margin-top: 4px; margin-bottom: 8px;" />
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600; padding-bottom: 32px;">{{tierData.name}}
{{#if tierData.details}} <span style="font-weight: normal; color:#3A464C;"> - {{tierData.details}}</span>{{/if}}
</p>
{{#if offerData}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Offer</p>
<hr style="border-bottom: 1px solid #F4F4F5; margin-top: 4px; margin-bottom: 8px;" />
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600; padding-bottom: 32px;">{{offerData.name}} <span style="font-weight: normal; color:#3A464C;"> - {{offerData.details}}</span></p>
{{/if}}
{{#if referrerSource}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Signup info</p>
<hr style="border-bottom: 1px solid #F4F4F5; margin-top: 4px; margin-bottom: 8px;" />
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600; padding-bottom: 4px;">Source
<span style="font-weight: normal; color:#3A464C;"> - {{referrerSource}}</span>
</p>
{{#if attributionTitle}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">Page
<span style="font-weight: normal; color:#3A464C;"> - <a href="{{attributionUrl}}" style="font-weight: normal; color:#3A464C;text-decoration:none">{{attributionTitle}}</a></span>
</p>
{{/if}}
{{/if}}
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>

View File

@ -27,8 +27,16 @@ class StaffServiceEmails {
const subject = `🥳 Free member signup: ${memberData.name}`;
let attributionTitle = attribution?.title || '';
// In case of a homepage attribution, we want to show the title as "Homepage" on email
if (attributionTitle === 'homepage') {
attributionTitle = 'Homepage';
}
const templateData = {
memberData,
attributionTitle,
attributionUrl: attribution?.url || '',
referrerSource: attribution?.referrerSource,
siteTitle: this.settingsCache.get('title'),
siteUrl: this.urlUtils.getSiteUrl(),
@ -73,8 +81,16 @@ class StaffServiceEmails {
let offerData = this.getOfferData(offer);
let attributionTitle = attribution?.title || '';
// In case of a homepage attribution, we want to show the title as "Homepage" on email
if (attributionTitle === 'homepage') {
attributionTitle = 'Homepage';
}
const templateData = {
memberData,
attributionTitle,
attributionUrl: attribution?.url || '',
referrerSource: attribution?.referrerSource,
tierData,
offerData,

View File

@ -5,13 +5,14 @@ const {MilestoneCreatedEvent} = require('@tryghost/milestones');
// @NOTE: 'StaffService' is a vague name that does not describe what it's actually doing.
// Possibly, "StaffNotificationService" or "StaffEventNotificationService" would be a more accurate name
class StaffService {
constructor({logging, models, mailer, settingsCache, settingsHelpers, urlUtils, DomainEvents, labs}) {
constructor({logging, models, mailer, settingsCache, settingsHelpers, urlUtils, DomainEvents, labs, memberAttributionService}) {
this.logging = logging;
this.labs = labs;
/** @private */
this.settingsCache = settingsCache;
this.models = models;
this.DomainEvents = DomainEvents;
this.memberAttributionService = memberAttributionService;
const Emails = require('./emails');
@ -98,17 +99,29 @@ class StaffService {
});
if (type === MemberCreatedEvent && member.status === 'free') {
let attribution;
try {
attribution = await this.memberAttributionService.getMemberCreatedAttribution(event.data.memberId);
} catch (e) {
this.logging.warn(`Failed to get attribution for member - ${event?.data?.memberId}`);
}
await this.emails.notifyFreeMemberSignup({
member,
attribution: event?.data?.attribution
attribution
});
} else if (type === SubscriptionActivatedEvent) {
let attribution;
try {
attribution = await this.memberAttributionService.getSubscriptionCreatedAttribution(event.data.subscriptionId);
} catch (e) {
this.logging.warn(`Failed to get attribution for member - ${event?.data?.memberId}`);
}
await this.emails.notifyPaidSubscriptionStarted({
member,
offer,
tier,
subscription,
attribution: event?.data?.attribution
attribution
});
} else if (type === SubscriptionCancelledEvent) {
subscription.canceledAt = event.timestamp;

View File

@ -429,7 +429,9 @@ describe('StaffService', function () {
};
const attribution = {
referrerSource: 'Twitter'
referrerSource: 'Twitter',
title: 'Welcome Post',
url: 'https://example.com/welcome'
};
await service.emails.notifyFreeMemberSignup({member, attribution}, options);
@ -447,8 +449,23 @@ describe('StaffService', function () {
mailStub.calledWith(
sinon.match.has('html', sinon.match('Created on 1 Aug 2022 &#8226; France'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Source: Twitter'))
sinon.match.has('html', sinon.match('Source'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Twitter'))
).should.be.true();
// check attribution page
mailStub.calledWith(
sinon.match.has('html', sinon.match('Welcome Post'))
).should.be.true();
// check attribution url
mailStub.calledWith(
sinon.match.has('html', sinon.match('https://example.com/welcome'))
).should.be.true();
});
});
@ -486,7 +503,9 @@ describe('StaffService', function () {
it('sends paid subscription start alert with attribution', async function () {
const attribution = {
referrerSource: 'Twitter'
referrerSource: 'Twitter',
title: 'Welcome Post',
url: 'https://example.com/welcome'
};
await service.emails.notifyPaidSubscriptionStarted({member, offer: null, tier, subscription, attribution}, options);
@ -495,7 +514,22 @@ describe('StaffService', function () {
// check attribution text
mailStub.calledWith(
sinon.match.has('html', sinon.match('Source: Twitter'))
sinon.match.has('html', sinon.match('Twitter'))
).should.be.true();
// check attribution text
mailStub.calledWith(
sinon.match.has('html', sinon.match('Source'))
).should.be.true();
// check attribution page
mailStub.calledWith(
sinon.match.has('html', sinon.match('Welcome Post'))
).should.be.true();
// check attribution url
mailStub.calledWith(
sinon.match.has('html', sinon.match('https://example.com/welcome'))
).should.be.true();
});