diff --git a/ghost/admin/app/components/settings/analytics.hbs b/ghost/admin/app/components/settings/analytics.hbs
new file mode 100644
index 0000000000..414b124d5f
--- /dev/null
+++ b/ghost/admin/app/components/settings/analytics.hbs
@@ -0,0 +1,78 @@
+
+
Newsletters
+
+
+
+
+
Track newsletter opens
+
+ Record when member opens a newsletter email
+
+
+
+
+
+
+
+
+
+
+
+
Track newsletter clicks
+
+ Record when a member clicks on any link in a newsletter email
+
+
+
+
+
+
+
+
+
+
+
+
Sources
+
+
+
+
+
Track member sources
+
+ Record the post/page and referring domain for signups and subscriptions
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ghost/admin/app/components/settings/analytics.js b/ghost/admin/app/components/settings/analytics.js
new file mode 100644
index 0000000000..270f12c684
--- /dev/null
+++ b/ghost/admin/app/components/settings/analytics.js
@@ -0,0 +1,33 @@
+import Component from '@glimmer/component';
+import {action} from '@ember/object';
+import {inject as service} from '@ember/service';
+
+export default class Analytics extends Component {
+ @service config;
+ @service settings;
+ @service feature;
+
+ @action
+ toggleEmailTrackOpens(event) {
+ if (event) {
+ event.preventDefault();
+ }
+ this.settings.emailTrackOpens = !this.settings.emailTrackOpens;
+ }
+
+ @action
+ toggleEmailTrackClicks(event) {
+ if (event) {
+ event.preventDefault();
+ }
+ this.settings.emailTrackClicks = !this.settings.emailTrackClicks;
+ }
+
+ @action
+ toggleMembersTrackSources(event) {
+ if (event) {
+ event.preventDefault();
+ }
+ this.settings.membersTrackSources = !this.settings.membersTrackSources;
+ }
+}
diff --git a/ghost/admin/app/controllers/settings/analytics.js b/ghost/admin/app/controllers/settings/analytics.js
index 0bd6769027..2f51689d40 100644
--- a/ghost/admin/app/controllers/settings/analytics.js
+++ b/ghost/admin/app/controllers/settings/analytics.js
@@ -1,170 +1,13 @@
-import classic from 'ember-classic-decorator';
-import {action, computed} from '@ember/object';
-import {inject as service} from '@ember/service';
-/* eslint-disable ghost/ember/alias-model-in-controller */
import Controller from '@ember/controller';
-import generatePassword from 'ghost-admin/utils/password-generator';
-import {
- IMAGE_EXTENSIONS,
- IMAGE_MIME_TYPES
-} from 'ghost-admin/components/gh-image-uploader';
-import {TrackedObject} from 'tracked-built-ins';
-import {run} from '@ember/runloop';
+import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
-import {tracked} from '@glimmer/tracking';
-function randomPassword() {
- let word = generatePassword(6);
- let randomN = Math.floor(Math.random() * 1000);
-
- return word + randomN;
-}
-
-@classic
export default class AnalyticsController extends Controller {
- @service config;
- @service ghostPaths;
- @service notifications;
- @service session;
@service settings;
- @service frontend;
- @service ui;
- @tracked scratchValues = new TrackedObject();
-
- availableTimezones = this.config.availableTimezones;
- imageExtensions = IMAGE_EXTENSIONS;
- imageMimeTypes = IMAGE_MIME_TYPES;
-
- @computed('config.blogUrl', 'settings.publicHash')
- get privateRSSUrl() {
- let blogUrl = this.config.blogUrl;
- let publicHash = this.settings.publicHash;
-
- return `${blogUrl}/${publicHash}/rss`;
- }
-
- @action
- save() {
- this.saveTask.perform();
- }
-
- @action
- setTimezone(timezone) {
- this.settings.timezone = timezone.name;
- }
-
- @action
- removeImage(image) {
- // setting `null` here will error as the server treats it as "null"
- this.settings[image] = '';
- }
-
- /**
- * Opens a file selection dialog - Triggered by "Upload Image" buttons,
- * searches for the hidden file input within the .gh-setting element
- * containing the clicked button then simulates a click
- * @param {MouseEvent} event - MouseEvent fired by the button click
- */
- @action
- triggerFileDialog(event) {
- event?.target.closest('.gh-setting-action')?.querySelector('input[type="file"]')?.click();
- }
-
- /**
- * Fired after an image upload completes
- * @param {string} property - Property name to be set on `this.settings`
- * @param {UploadResult[]} results - Array of UploadResult objects
- * @return {string} The URL that was set on `this.settings.property`
- */
- @action
- imageUploaded(property, results) {
- if (results[0]) {
- return this.settings[property] = results[0].url;
- }
- }
-
- @action
- toggleIsPrivate(isPrivate) {
- let settings = this.settings;
-
- settings.isPrivate = isPrivate;
- settings.errors.remove('password');
-
- let changedAttrs = settings.changedAttributes();
-
- // set a new random password when isPrivate is enabled
- if (isPrivate && changedAttrs.isPrivate) {
- settings.password = randomPassword();
-
- // reset the password when isPrivate is disabled
- } else if (changedAttrs.password) {
- settings.password = changedAttrs.password[0];
- }
- }
-
- @action
- setScratchValue(property, value) {
- this.scratchValues[property] = value;
- }
-
- clearScratchValues() {
- this.scratchValues = new TrackedObject();
- }
-
- _deleteTheme() {
- let theme = this.store.peekRecord('theme', this.themeToDelete.name);
-
- if (!theme) {
- return;
- }
-
- return theme.destroyRecord().catch((error) => {
- this.notifications.showAPIError(error);
- });
- }
-
- @task
- *saveTask() {
- let notifications = this.notifications;
- let config = this.config;
-
- try {
- let changedAttrs = this.settings.changedAttributes();
- let settings = yield this.settings.save();
-
- this.clearScratchValues();
-
- config.set('blogTitle', settings.title);
-
- if (changedAttrs.password) {
- this.frontend.loginIfNeeded();
- }
-
- // this forces the document title to recompute after a blog title change
- this.ui.updateDocumentTitle();
-
- return settings;
- } catch (error) {
- if (error) {
- notifications.showAPIError(error, {key: 'settings.save'});
- }
- throw error;
- }
- }
-
- @action
- saveViaKeyboard(event) {
- event.preventDefault();
-
- // trigger any set-on-blur actions
- const focusedElement = document.activeElement;
- focusedElement?.blur();
-
- // schedule save for when set-on-blur actions have finished
- run.schedule('actions', this, function () {
- focusedElement?.focus();
- this.saveTask.perform();
- });
+ @task({drop: true})
+ *saveSettings() {
+ const response = yield this.settings.save();
+ return response;
}
}
diff --git a/ghost/admin/app/models/setting.js b/ghost/admin/app/models/setting.js
index 490ac7de4f..56caa2da68 100644
--- a/ghost/admin/app/models/setting.js
+++ b/ghost/admin/app/models/setting.js
@@ -59,6 +59,7 @@ export default Model.extend(ValidationEngine, {
membersSupportAddress: attr('string'),
membersMonthlyPriceId: attr('string'),
membersYearlyPriceId: attr('string'),
+ membersTrackSources: attr('boolean'),
stripeSecretKey: attr('string'),
stripePublishableKey: attr('string'),
stripePlans: attr('json-string'),
diff --git a/ghost/admin/app/templates/settings/analytics.hbs b/ghost/admin/app/templates/settings/analytics.hbs
index ee5d901396..60fd6f4ee0 100644
--- a/ghost/admin/app/templates/settings/analytics.hbs
+++ b/ghost/admin/app/templates/settings/analytics.hbs
@@ -1,4 +1,4 @@
-