Cleaned up suppressionList feature flag (#16736)

no issue

This pull request removes the `suppressionList` feature flag and all its
dependencies from the codebase. It makes the suppression list feature
the default and consistent behavior for all email events and
newsletters. It simplifies the UI, logic, and data related to email
events and newsletters. It affects several files in the
`ghost/admin/app`, `ghost/core/core`, and `ghost/members-api`
directories.
This commit is contained in:
Simon Backx 2023-05-04 14:47:04 +02:00 committed by GitHub
parent 17a6217cc7
commit 848b2d82a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 55 additions and 207 deletions

View File

@ -64,29 +64,6 @@
<p> Maximum: <b>500</b> characters. Youve used
{{gh-count-down-characters this.scratchMember.note 500}}</p>
</GhFormGroup>
{{#if this.canShowSingleNewsletter}}
<GhFormGroup class="gh-members-subscribed-checkbox mb0">
<div class="flex justify-between items-center" data-test-member-settings-switch>
<div>
<h4 class="gh-setting-title m">Subscribed to newsletter</h4>
<p class="gh-setting-desc">If disabled, member will <em>not</em> receive newsletter emails</p>
</div>
<div class="for-switch">
<label class="switch" for="subscribed-checkbox">
<Input
@checked={{this.member.subscribed}}
@type="checkbox"
id="subscribed-checkbox"
name="subscribed"
{{on "click" this.updateNewsletterPreference}}
data-test-checkbox="member-subscribed"
/>
<span class="input-toggle-component"></span>
</label>
</div>
</div>
</GhFormGroup>
{{/if}}
</div>
</div>

View File

@ -116,18 +116,9 @@ export default class extends Component {
return null;
}
get canShowSingleNewsletter() {
return (
this.newslettersList?.length === 1
&& this.settings.editorDefaultEmailRecipients !== 'disabled'
&& !this.feature.get('suppressionList')
);
}
get canShowMultipleNewsletters() {
return (
(this.newslettersList?.length > 1 || this.feature.get('suppressionList'))
&& this.settings.editorDefaultEmailRecipients !== 'disabled'
this.settings.editorDefaultEmailRecipients !== 'disabled'
);
}

View File

@ -6,9 +6,7 @@ export default class ActivityFeed extends Component {
@service feature;
linkScrollerTimeout = null; // needs to be global so can be cleared when needed across functions
excludedEventTypes = this.feature.get('suppressionList')
? ['aggregated_click_event']
: ['email_sent_event', 'aggregated_click_event'];
excludedEventTypes = ['aggregated_click_event'];
@action
enterLinkURL(event) {

View File

@ -1,5 +1,5 @@
<h4 class="gh-main-section-header small bn">Newsletters</h4>
<div class="gh-main-section-content grey {{if (and (feature "suppressionList") (not this.suppressionData.suppressed)) 'gh-member-newsletter-section'}}">
<div class="gh-main-section-content grey {{unless this.suppressionData.suppressed 'gh-member-newsletter-section'}}">
{{#unless this.suppressionData.suppressed}}
<div class="gh-member-newsletters">
{{#each this.newsletters as |newsletter|}}
@ -7,7 +7,7 @@
<div>
<h4 class="gh-member-newsletter-title">{{newsletter.name}}</h4>
</div>
<div class="for-switch {{if (and (feature "suppressionList") this.suppressionData.suppressed) 'disabled'}}">
<div class="for-switch {{if this.suppressionData.suppressed 'disabled'}}">
<label class="switch" for={{newsletter.forId}}>
<Input
@checked={{newsletter.subscribed}}
@ -25,7 +25,7 @@
</div>
{{/unless}}
{{#if (and (feature "suppressionList") this.suppressionData.suppressed)}}
{{#if this.suppressionData.suppressed}}
<div class="gh-members-no-data gh-member-newsletter-no-data">
{{#if (eq this.suppressionData.reason 'fail')}}
{{svg-jar "suppression-notice-bounced" class="gh-member-newsletter-icon"}}
@ -49,9 +49,7 @@
<a class="midgrey" href="https://ghost.org/help/disabled-emails" target="_blank" rel="noopener noreferrer">Learn more</a>
</p>
</div>
{{/if}}
{{#if (and (feature "suppressionList") (not this.suppressionData.suppressed))}}
{{else}}
<div class="gh-member-newsletter-footer midgrey">
If disabled, member will <em>not</em> receive newsletter emails
</div>

View File

@ -6,13 +6,20 @@
</span>
</dd.Trigger>
<dd.Content class="gh-member-activity-actions-menu dropdown-menu dropdown-triangle-top-right {{if (feature "suppressionList") 'gh-member-activity-actions-menu--suppression'}}">
<dd.Content class="gh-member-activity-actions-menu dropdown-menu dropdown-triangle-top-right gh-member-activity-actions-menu--suppression">
{{!-- NOTE: re-using ember-power-select-options styles --}}
<ul class="ember-power-select-options" role="listbox">
{{#each this.eventTypes as |type idx|}}
{{#if (not (feature "suppressionList"))}}
<li class="form-group ember-power-select-option mb0 for-checkbox">
<label class="checkbox" for="type-{{idx}}">
{{#if type.divider}}
<li class="gh-member-activity-actions-menu-divider"></li>
{{/if}}
<li class="ember-power-select-option mb0 gh-member-activity-actions-menu-item">
<label for="type-{{idx}}">
{{svg-jar type.icon class="gh-member-activity-actions-menu-icon"}}
<span>{{type.name}}</span>
</label>
<div class="for-switch x-small">
<label class="switch" for="type-{{idx}}">
<input
type="checkbox"
checked={{type.isSelected}}
@ -21,35 +28,10 @@
class="gh-input post-settings-featured"
{{on "input" (fn this.toggleEventType type.event)}}>
<span class="input-toggle-component"></span>
<p>{{type.name}}</p>
</label>
</li>
{{/if}}
{{#if (feature "suppressionList")}}
{{#if type.divider}}
<li class="gh-member-activity-actions-menu-divider"></li>
{{/if}}
<li class="ember-power-select-option mb0 gh-member-activity-actions-menu-item">
<label for="type-{{idx}}">
{{svg-jar type.icon class="gh-member-activity-actions-menu-icon"}}
<span>{{type.name}}</span>
</label>
<div class="for-switch x-small">
<label class="switch" for="type-{{idx}}">
<input
type="checkbox"
checked={{type.isSelected}}
id="type-{{idx}}"
name="eventTypes"
class="gh-input post-settings-featured"
{{on "input" (fn this.toggleEventType type.event)}}>
<span class="input-toggle-component"></span>
</label>
</div>
</li>
{{/if}}
</div>
</li>
{{/each}}
</ul>
</dd.Content>
</GhBasicDropdown>
</GhBasicDropdown>

View File

@ -2,19 +2,7 @@ import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
// todo: replace function with const after suppressionList feature flag will be removed
const ALL_EVENT_TYPES = [
{event: 'signup_event', icon: 'filter-dropdown-signups', name: 'Signups'},
{event: 'login_event', icon: 'filter-dropdown-logins', name: 'Logins'},
{event: 'subscription_event', icon: 'filter-dropdown-paid-subscriptions', name: 'Paid subscriptions'},
{event: 'payment_event', icon: 'filter-dropdown-payments', name: 'Payments'},
{event: 'newsletter_event', icon: 'filter-dropdown-email-subscriptions', name: 'Email subscriptions'},
{event: 'email_opened_event', icon: 'filter-dropdown-email-opened', name: 'Email opens'},
{event: 'email_delivered_event', icon: 'filter-dropdown-email-received', name: 'Email deliveries'},
{event: 'email_failed_event', icon: 'filter-dropdown-email-bounced', name: 'Email failures'}
];
const ALL_EVENT_TYPES_SUPPRESSION = [
{event: 'signup_event', icon: 'filter-dropdown-signups', name: 'Signups', group: 'auth'},
{event: 'login_event', icon: 'filter-dropdown-logins', name: 'Logins', group: 'auth'},
{event: 'subscription_event', icon: 'filter-dropdown-paid-subscriptions', name: 'Paid subscriptions', group: 'payments'},
@ -31,8 +19,7 @@ export default class MembersActivityEventTypeFilter extends Component {
@service feature;
get availableEventTypes() {
// todo: remove condition when feature will be enabled
const extended = this.feature.suppressionList ? [...ALL_EVENT_TYPES_SUPPRESSION] : [...ALL_EVENT_TYPES];
const extended = ALL_EVENT_TYPES;
if (this.settings.commentsEnabled !== 'off') {
extended.push({event: 'comment_event', icon: 'filter-dropdown-comments', name: 'Comments', group: 'others'});

View File

@ -1,7 +1,7 @@
import Component from '@glimmer/component';
import moment from 'moment-timezone';
import nql from '@tryghost/nql-lang';
import {AUDIENCE_FEEDBACK_FILTER, CREATED_AT_FILTER, EMAIL_CLICKED_FILTER, EMAIL_COUNT_FILTER, EMAIL_FILTER, EMAIL_OPENED_COUNT_FILTER, EMAIL_OPENED_FILTER, EMAIL_OPEN_RATE_FILTER, EMAIL_RECEIVED_FILTER, EMAIL_SENT_FILTER, LABEL_FILTER, LAST_SEEN_FILTER, NAME_FILTER, NEWSLETTERS_FILTER, NEXT_BILLING_DATE_FILTER, OFFERS_FILTER, PLAN_INTERVAL_FILTER, SIGNUP_ATTRIBUTION_FILTER, STATUS_FILTER, SUBSCRIBED_FILTER, SUBSCRIPTION_ATTRIBUTION_FILTER, SUBSCRIPTION_START_DATE_FILTER, SUBSCRIPTION_STATUS_FILTER, TIER_FILTER} from './filters';
import {AUDIENCE_FEEDBACK_FILTER, CREATED_AT_FILTER, EMAIL_CLICKED_FILTER, EMAIL_COUNT_FILTER, EMAIL_FILTER, EMAIL_OPENED_COUNT_FILTER, EMAIL_OPENED_FILTER, EMAIL_OPEN_RATE_FILTER, EMAIL_SENT_FILTER, LABEL_FILTER, LAST_SEEN_FILTER, NAME_FILTER, NEWSLETTERS_FILTER, NEXT_BILLING_DATE_FILTER, OFFERS_FILTER, PLAN_INTERVAL_FILTER, SIGNUP_ATTRIBUTION_FILTER, STATUS_FILTER, SUBSCRIBED_FILTER, SUBSCRIPTION_ATTRIBUTION_FILTER, SUBSCRIPTION_START_DATE_FILTER, SUBSCRIPTION_STATUS_FILTER, TIER_FILTER} from './filters';
import {TrackedArray} from 'tracked-built-ins';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
@ -43,7 +43,6 @@ const FILTER_GROUPS = [
EMAIL_COUNT_FILTER,
EMAIL_OPENED_COUNT_FILTER,
EMAIL_OPEN_RATE_FILTER,
EMAIL_RECEIVED_FILTER,
EMAIL_SENT_FILTER,
EMAIL_OPENED_FILTER,
EMAIL_CLICKED_FILTER,
@ -145,7 +144,7 @@ export default class MembersFilter extends Component {
]);
newsletters;
get filterProperties() {
let availableFilters = FILTER_PROPERTIES;
@ -167,7 +166,6 @@ export default class MembersFilter extends Component {
// exclude any filters that are behind disabled feature flags
availableFilters = availableFilters.filter(prop => !prop.feature || this.feature[prop.feature]);
availableFilters = availableFilters.filter(prop => !prop.setting || this.settings[prop.setting]);
availableFilters = availableFilters.filter(prop => !prop.excludeForFeature || !this.feature[prop.excludeForFeature]);
return availableFilters;
}

View File

@ -1,16 +0,0 @@
import {MATCH_RELATION_OPTIONS} from './relation-options';
export const EMAIL_RECEIVED_FILTER = {
label: 'Received email',
name: 'emails.post_id',
valueType: 'string',
resource: 'email',
excludeForFeature: 'suppressionList',
relationOptions: MATCH_RELATION_OPTIONS,
columnLabel: 'Received email',
getColumnValue: (member, filter) => {
return {
text: filter.resource?.title ?? ''
};
}
};

View File

@ -5,7 +5,6 @@ export const EMAIL_SENT_FILTER = {
name: 'emails.post_id',
valueType: 'string',
resource: 'email',
feature: 'suppressionList',
relationOptions: MATCH_RELATION_OPTIONS,
columnLabel: 'Sent email',
getColumnValue: (member, filter) => {

View File

@ -18,7 +18,6 @@ export * from './email-clicked';
export * from './email-opened-count';
export * from './email-open-rate';
export * from './email-clicked';
export * from './email-received';
export * from './email-sent';
export * from './audience-feedback';
export * from './offers';

View File

@ -29,18 +29,5 @@
<span class="gh-feedback-events-tooltip-metric">{{this.tooltipData.label}}</span>
</div>
{{#if (and this.tooltipData.href (not (feature "suppressionList")))}}
<div class="gh-feedback-events-tooltip-footer">
<LinkTo
class="gh-post-activity-feed-pagination-link gh-post-activity-chart-link"
@route="members"
@query={{this.tooltipData.href}}
>
{{svg-jar "filter"}}
<span>View members</span>
</LinkTo>
</div>
{{/if}}
</div>
</div>

View File

@ -84,23 +84,10 @@
{{/if}}
<div class="gh-post-activity-feed-footer">
{{#if (feature "suppressionList")}}
<Posts::PostActivityFeed::FooterLinks
@eventType={{this.eventType}}
@post={{@post}}
/>
{{/if}}
{{#if (and @linkQuery (not (feature "suppressionList")))}}
<LinkTo
class="gh-post-activity-feed-pagination-link"
@route="members"
@query={{hash @linkQuery postAnalytics=@post.id}}
>
{{svg-jar "filter"}}
View members
</LinkTo>
{{/if}}
<Posts::PostActivityFeed::FooterLinks
@eventType={{this.eventType}}
@post={{@post}}
/>
<div class="gh-post-activity-feed-pagination">
{{#if (compute (fn this.isPaginationNotNeeded eventsFetcher))}}

View File

@ -7,9 +7,7 @@ export default class PostActivityFeed extends Component {
_pageSize = 5;
_eventTypes = {
sent: this.feature.get('suppressionList')
? ['email_sent_event', 'email_delivered_event', 'email_failed_event']
: ['email_sent_event'],
sent: ['email_sent_event', 'email_delivered_event', 'email_failed_event'],
opened: ['email_opened_event'],
clicked: ['aggregated_click_event'],
feedback: ['feedback_event'],

View File

@ -27,10 +27,6 @@ export default class MembersActivityController extends Controller {
if (!this.member) {
hiddenEvents.push(...EMAIL_EVENTS);
} else {
if (!this.feature.get('suppressionList')) {
hiddenEvents.push('email_sent_event');
}
}
hiddenEvents.push('aggregated_click_event');

View File

@ -78,18 +78,12 @@ export default class ParseMemberEventHelper extends Helper {
icon = 'opened-email';
}
if (this.feature.get('suppressionList')) {
if (event.type === 'email_sent_event') {
icon = 'sent-email';
}
if (event.type === 'email_sent_event') {
icon = 'sent-email';
}
if (event.type === 'email_delivered_event') {
icon = 'received-email';
}
} else {
if (event.type === 'email_delivered_event' || event.type === 'email_sent_event') {
icon = 'received-email';
}
if (event.type === 'email_delivered_event') {
icon = 'received-email';
}
if (event.type === 'email_failed_event') {
@ -169,22 +163,16 @@ export default class ParseMemberEventHelper extends Helper {
return 'opened email';
}
if (this.feature.get('suppressionList')) {
if (event.type === 'email_sent_event') {
return 'sent email';
}
if (event.type === 'email_sent_event') {
return 'sent email';
}
if (event.type === 'email_delivered_event') {
return 'received email';
}
} else {
if (event.type === 'email_delivered_event' || event.type === 'email_sent_event') {
return 'received email';
}
if (event.type === 'email_delivered_event') {
return 'received email';
}
if (event.type === 'email_failed_event') {
return this.feature.get('suppressionList') ? 'bounced email' : 'failed to receive email';
return 'bounced email';
}
if (event.type === 'email_complaint_event') {

View File

@ -65,7 +65,6 @@ export default class FeatureService extends Service {
@feature('lexicalEditor') lexicalEditor;
@feature('lexicalMultiplayer') lexicalMultiplayer;
@feature('audienceFeedback') audienceFeedback;
@feature('suppressionList') suppressionList;
@feature('webmentions') webmentions;
@feature('emailErrors') emailErrors;
@feature('websockets') websockets;

View File

@ -2,7 +2,6 @@
const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output:members');
const {unparse} = require('@tryghost/members-csv');
const mappers = require('./mappers');
const labs = require('../../../../../../shared/labs');
module.exports = {
browse: createSerializer('browse', paginatedMembers),
@ -172,9 +171,7 @@ function serializeMember(member, options) {
delete subscription.price.product;
}
if (labs.isSet('suppressionList')) {
serialized.email_suppression = json.email_suppression;
}
serialized.email_suppression = json.email_suppression;
if (json.newsletters) {
serialized.newsletters = json.newsletters

View File

@ -1,5 +1,3 @@
const labsService = require('../../../shared/labs');
function formatNewsletterResponse(newsletters) {
return newsletters.map(({id, name, description, sort_order: sortOrder}) => {
return {id, name, description, sort_order: sortOrder};
@ -27,7 +25,7 @@ module.exports.formattedMemberResponse = function formattedMemberResponse(member
data.newsletters = formatNewsletterResponse(member.newsletters);
}
if (labsService.isSet('suppressionList') && member.email_suppression) {
if (member.email_suppression) {
data.email_suppression = member.email_suppression;
}

View File

@ -47,7 +47,7 @@ module.exports = function setupMembersApp() {
membersApp.post('/api/member/email', bodyParser.json({limit: '50mb'}), (req, res) => membersService.api.middleware.updateEmailAddress(req, res));
// Remove email from suppression list
membersApp.delete('/api/member/suppression', labs.enabledMiddleware('suppressionList'), middleware.deleteSuppression);
membersApp.delete('/api/member/suppression', middleware.deleteSuppression);
// Manage session
membersApp.get('/api/session', middleware.getIdentityToken);

View File

@ -15,7 +15,6 @@ const messages = {
// flags in this list always return `true`, allows quick global enable prior to full flag removal
const GA_FEATURES = [
'suppressionList',
'sourceAttribution',
'memberAttribution',
'audienceFeedback',

View File

@ -718,7 +718,7 @@ exports[`Settings API Edit Can edit a setting 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "4093",
"content-length": "4068",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -351,9 +351,6 @@ module.exports = function MembersAPI({
bus.emit('ready');
DomainEvents.subscribe(EmailSuppressedEvent, async function (event) {
if (!labsService.isSet('suppressionList')) {
return;
}
const member = await memberRepository.get({email: event.data.emailAddress});
if (!member) {
return;

View File

@ -84,9 +84,7 @@ module.exports = class EventRepository {
pageActions.push({type: 'email_failed_event', action: 'getEmailFailedEvents'});
}
if (this._labsService.isSet('suppressionList')) {
pageActions.push({type: 'email_complained_event', action: 'getEmailSpamComplaintEvents'});
}
pageActions.push({type: 'email_complained_event', action: 'getEmailSpamComplaintEvents'});
if (this._labsService.isSet('audienceFeedback')) {
pageActions.push({type: 'feedback_event', action: 'getFeedbackEvents'});
@ -505,9 +503,7 @@ module.exports = class EventRepository {
}
async getEmailSentEvents(options = {}, filter) {
const filterStr = this._labsService.isSet('suppressionList')
? 'failed_at:null+processed_at:-null+delivered_at:null+custom:true'
: 'failed_at:null+processed_at:-null+custom:true';
const filterStr = 'failed_at:null+processed_at:-null+delivered_at:null+custom:true';
options = {
...options,
withRelated: ['member', 'email'],

View File

@ -243,13 +243,11 @@ module.exports = class MemberBREADService {
await this.attachAttributionsToMember(member, subscriptionIdMap);
}
if (this.labsService.isSet('suppressionList')) {
const suppressionData = await this.emailSuppressionList.getSuppressionData(member.email);
member.email_suppression = {
suppressed: suppressionData.suppressed,
info: suppressionData.info
};
}
const suppressionData = await this.emailSuppressionList.getSuppressionData(member.email);
member.email_suppression = {
suppressed: suppressionData.suppressed,
info: suppressionData.info
};
return member;
}
@ -402,10 +400,7 @@ module.exports = class MemberBREADService {
const members = page.data.map(model => model.toJSON(options));
let bulkSuppressionData;
if (this.labsService.isSet('suppressionList')) {
bulkSuppressionData = await this.emailSuppressionList.getBulkSuppressionData(members.map(member => member.email));
}
const bulkSuppressionData = await this.emailSuppressionList.getBulkSuppressionData(members.map(member => member.email));
const data = members.map((member, index) => {
member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
@ -414,12 +409,10 @@ module.exports = class MemberBREADService {
if (!originalWithRelated.includes('products')) {
delete member.products;
}
if (this.labsService.isSet('suppressionList')) {
member.email_suppression = {
suppressed: bulkSuppressionData[index].suppressed,
info: bulkSuppressionData[index].info
};
}
member.email_suppression = {
suppressed: bulkSuppressionData[index].suppressed,
info: bulkSuppressionData[index].info
};
return member;
});