Skip to content

Commit

Permalink
feat(metrics): add product_id and plan_id to more amplitude events
Browse files Browse the repository at this point in the history
Made the Flow model follow Backbone conventions more closely
where the first argument to the constructor is attributes and
the second is options.

More robust checking when fetching the product ID from the
path. The plan_id is now optional.

Combined effort of @philbooth and @shane-tomlinson, Phil did thet hard work.

issue #989
  • Loading branch information
philbooth authored and shane-tomlinson committed Oct 15, 2019
1 parent 8716f77 commit ed501fa
Show file tree
Hide file tree
Showing 20 changed files with 412 additions and 42 deletions.
4 changes: 3 additions & 1 deletion packages/fxa-content-server/.prettierrc
@@ -1,4 +1,6 @@
{ {
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5" "trailingComma": "es5",
"tabWidth": 2,
"useTabs": false
} }
47 changes: 37 additions & 10 deletions packages/fxa-content-server/app/scripts/lib/metrics.js
Expand Up @@ -21,10 +21,11 @@ import Constants from './constants';
import Backbone from 'backbone'; import Backbone from 'backbone';
import Duration from 'duration'; import Duration from 'duration';
import Environment from './environment'; import Environment from './environment';
import Flow from '../models/flow'; import FlowModel from '../models/flow';
import NotifierMixin from './channels/notifier-mixin'; import NotifierMixin from './channels/notifier-mixin';
import speedTrap from 'speed-trap'; import speedTrap from 'speed-trap';
import Strings from './strings'; import Strings from './strings';
import SubscriptionModel from 'models/subscription';
import xhr from './xhr'; import xhr from './xhr';


// Speed trap is a singleton, convert it // Speed trap is a singleton, convert it
Expand Down Expand Up @@ -53,8 +54,8 @@ const ALLOWED_FIELDS = [
'migration', 'migration',
'navigationTiming', 'navigationTiming',
'numStoredAccounts', 'numStoredAccounts',
'plan_id', 'planId',
'product_id', 'productId',
'reason', 'reason',
'referrer', 'referrer',
'screen', 'screen',
Expand Down Expand Up @@ -153,8 +154,6 @@ function Metrics(options = {}) {
this._marketingImpressions = {}; this._marketingImpressions = {};
this._migration = options.migration || NOT_REPORTED_VALUE; this._migration = options.migration || NOT_REPORTED_VALUE;
this._numStoredAccounts = options.numStoredAccounts || ''; this._numStoredAccounts = options.numStoredAccounts || '';
this._planId = options.planId || NOT_REPORTED_VALUE;
this._productId = options.productId || NOT_REPORTED_VALUE;
this._referrer = this._window.document.referrer || NOT_REPORTED_VALUE; this._referrer = this._window.document.referrer || NOT_REPORTED_VALUE;
this._screenHeight = options.screenHeight || NOT_REPORTED_VALUE; this._screenHeight = options.screenHeight || NOT_REPORTED_VALUE;
this._screenWidth = options.screenWidth || NOT_REPORTED_VALUE; this._screenWidth = options.screenWidth || NOT_REPORTED_VALUE;
Expand Down Expand Up @@ -190,6 +189,8 @@ _.extend(Metrics.prototype, Backbone.Events, {


// Set the initial inactivity timeout to clear navigation timing data. // Set the initial inactivity timeout to clear navigation timing data.
this._resetInactivityFlushTimeout(); this._resetInactivityFlushTimeout();

this._initializeSubscriptionModel();
}, },


destroy() { destroy() {
Expand All @@ -205,7 +206,7 @@ _.extend(Metrics.prototype, Backbone.Events, {
'set-email-domain': '_setEmailDomain', 'set-email-domain': '_setEmailDomain',
'set-sync-engines': '_setSyncEngines', 'set-sync-engines': '_setSyncEngines',
'set-uid': '_setUid', 'set-uid': '_setUid',
'set-plan-and-product-id': '_setPlanProductId', 'subscription.initialize': '_initializeSubscriptionModel',
'clear-uid': '_clearUid', 'clear-uid': '_clearUid',
'once!view-shown': '_setInitialView', 'once!view-shown': '_setInitialView',
/* eslint-enable sorting/sort-object-props */ /* eslint-enable sorting/sort-object-props */
Expand All @@ -222,7 +223,7 @@ _.extend(Metrics.prototype, Backbone.Events, {
return; return;
} }


const flowModel = new Flow({ const flowModel = new FlowModel({
metrics: this, metrics: this,
sentryMetrics: this._sentryMetrics, sentryMetrics: this._sentryMetrics,
window: this._window, window: this._window,
Expand All @@ -233,6 +234,26 @@ _.extend(Metrics.prototype, Backbone.Events, {
} }
}, },


/**
* @private
* Initialise the subscription model.
*
* @param {Object} [model] model to initialise with.
* If unset, a fresh model is created.
*/
_initializeSubscriptionModel(model) {
if (model && model.has('productId')) {
this._subscriptionModel = model;
} else {
this._subscriptionModel = new SubscriptionModel(
{},
{
window: this._window,
}
);
}
},

/** /**
* @private * @private
* Log a flow event. If there is no flow model, do nothing. * Log a flow event. If there is no flow model, do nothing.
Expand Down Expand Up @@ -383,8 +404,10 @@ _.extend(Metrics.prototype, Backbone.Events, {
marketing: flattenHashIntoArrayOfObjects(this._marketingImpressions), marketing: flattenHashIntoArrayOfObjects(this._marketingImpressions),
migration: this._migration, migration: this._migration,
numStoredAccounts: this._numStoredAccounts, numStoredAccounts: this._numStoredAccounts,
plan_id: this._planId, // planId and productId are optional so we can physically remove
product_id: this._productId, // them from the payload instead of sending NOT_REPORTED_VALUE
planId: this._subscriptionModel.get('planId') || undefined,
productId: this._subscriptionModel.get('productId') || undefined,
referrer: this._referrer, referrer: this._referrer,
screen: { screen: {
clientHeight: this._clientHeight, clientHeight: this._clientHeight,
Expand Down Expand Up @@ -698,10 +721,14 @@ _.extend(Metrics.prototype, Backbone.Events, {
}; };
}, },


getFlowModel(flowModel) { getFlowModel() {
return this._flowModel; return this._flowModel;
}, },


getSubscriptionModel() {
return this._subscriptionModel;
},

/** /**
* Log the number of stored accounts * Log the number of stored accounts
* *
Expand Down
9 changes: 9 additions & 0 deletions packages/fxa-content-server/app/scripts/lib/url-mixin.js
Expand Up @@ -42,4 +42,13 @@ export default {
getHashParams(paramNames) { getHashParams(paramNames) {
return Url.hashParams(this.window.location.hash, paramNames); return Url.hashParams(this.window.location.hash, paramNames);
}, },

/**
* return the pathname of the window
*
* @returns {String}
*/
getPathname() {
return this.window.location.pathname;
},
}; };
Expand Up @@ -20,6 +20,8 @@ const ALLOWED_KEYS = [
'flowId', 'flowId',
'needsOptedInToMarketingEmail', 'needsOptedInToMarketingEmail',
'newsletters', 'newsletters',
'planId',
'productId',
'resetPasswordConfirm', 'resetPasswordConfirm',
'style', 'style',
'uniqueUserId', 'uniqueUserId',
Expand Down
101 changes: 101 additions & 0 deletions packages/fxa-content-server/app/scripts/models/subscription.ts
@@ -0,0 +1,101 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
* A model to represent current subscription state, so that metrics can
* associate events with products and plans.
*
* Tries to read data from the URL or, failing that, the resume token.
*/

import Backbone from 'backbone';
import Cocktail from '../lib/cocktail';
import ResumeTokenMixin from './mixins/resume-token';
import UrlMixin from './mixins/url';

const SUBSCRIBE_PRODUCT_PATHNAME_REGEXP = /^\/subscriptions\/products\/(\w+)/;

// Neither UrlMixin nor ResumeTokenMixin are classes or interfaces
interface IUrlMixin {
getSearchParam(paramName: string): string | undefined;
getPathname(): string;
}

interface IResumeTokenMixin {
populateFromStringifiedResumeToken(resumeToken: string);
}

const RESUME_TOKEN_FIELDS = ['planId', 'productId'];
class SubscriptionModel extends Backbone.Model {
window?: Window;
resumeTokenFields?: string[];

constructor(attrs = {}, options) {
super(
{
planId: null,
productId: null,
...attrs,
},
options
);
}

initialize(attrs = {}, options: { window?: Window } = {}) {
if (this.get('planId') && this.get('productId')) {
// already set, no need to look anywhere else for the values.
return;
}

this.window = options.window || window;
this.resumeTokenFields = RESUME_TOKEN_FIELDS;

this._setSubscriptionInfoFromUrl();
if (this.get('productId')) {
return;
}

this._setSubscriptionInfoFromResumeToken();
}

_setSubscriptionInfoFromUrl() {
const productId = this._getProductIdFromPathname(this.getPathname());
if (productId) {
// If the user is browsing directly to /subscriptions/prod_*
this.set('productId', productId);

// only get the planId from the query params if the user is at
// a product path. It's possible for the plan to not be
// specified, in which case the default plan will be used.
const planId = this.getSearchParam('plan');
if (planId) {
this.set('planId', planId);
}
}
}

_getProductIdFromPathname(pathname: string) {
const result = SUBSCRIBE_PRODUCT_PATHNAME_REGEXP.exec(pathname);
if (result) {
return result[1];
}
return;
}

_setSubscriptionInfoFromResumeToken() {
const resumeToken = this.getSearchParam('resume');
if (resumeToken) {
// this.resumeTokenFields is not set until the constructor
// completes, and this method can be called from within
// the constructor. Set the value
this.populateFromStringifiedResumeToken(resumeToken);
}
}
}

interface SubscriptionModel extends IUrlMixin, IResumeTokenMixin {}

Cocktail.mixin(SubscriptionModel, ResumeTokenMixin, UrlMixin);

export default SubscriptionModel;
Expand Up @@ -26,12 +26,15 @@ export default {
flowInfo = flowModel.pickResumeTokenInfo(); flowInfo = flowModel.pickResumeTokenInfo();
} }


const subscriptionModel = this.metrics.getSubscriptionModel();

var resumeTokenInfo = _.extend( var resumeTokenInfo = _.extend(
{}, {},
flowInfo, flowInfo,
relierInfo, relierInfo,
userInfo, userInfo,
accountInfo accountInfo,
subscriptionModel.pickResumeTokenInfo()
); );


return new ResumeToken(resumeTokenInfo); return new ResumeToken(resumeTokenInfo);
Expand Down
17 changes: 13 additions & 4 deletions packages/fxa-content-server/app/scripts/views/support.js
Expand Up @@ -18,6 +18,7 @@ import PaymentServer from '../lib/payment-server';
import preventDefaultThen from './decorators/prevent_default_then'; import preventDefaultThen from './decorators/prevent_default_then';
import SettingsHeaderTemplate from 'templates/partial/settings-header.mustache'; import SettingsHeaderTemplate from 'templates/partial/settings-header.mustache';
import Strings from '../lib/strings'; import Strings from '../lib/strings';
import SubscriptionModel from 'models/subscription';
import SupportForm from 'models/support-form'; import SupportForm from 'models/support-form';
import SupportFormErrorTemplate from 'templates/partial/support-form-error.mustache'; import SupportFormErrorTemplate from 'templates/partial/support-form-error.mustache';
import SupportFormSuccessTemplate from 'templates/partial/support-form-success.mustache'; import SupportFormSuccessTemplate from 'templates/partial/support-form-success.mustache';
Expand Down Expand Up @@ -138,10 +139,18 @@ const SupportView = BaseView.extend({
let productName = 'Other'; let productName = 'Other';
if (subhubPlan) { if (subhubPlan) {
productName = subhubPlan.product_name; productName = subhubPlan.product_name;
this.notifier.trigger('set-plan-and-product-id', { this.notifier.trigger(
planId: subhubPlan.plan_id, 'subscription.initialize',
productId: subhubPlan.product_id, new SubscriptionModel(
}); {
planId: subhubPlan.plan_id,
productId: subhubPlan.product_id,
},
{
window: this.window,
}
)
);
} }


this.supportForm.set({ this.supportForm.set({
Expand Down

0 comments on commit ed501fa

Please sign in to comment.