Skip to content
Permalink
Browse files

feat(metrics): add product_id and plan_id to more amplitude events

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 Sep 25, 2019
1 parent 8716f77 commit ed501fa1c3485ea33d47ea75ac3b047782fb7bc5
@@ -1,4 +1,6 @@
{
"singleQuote": true,
"trailingComma": "es5"
"trailingComma": "es5",
"tabWidth": 2,
"useTabs": false
}
@@ -21,10 +21,11 @@ import Constants from './constants';
import Backbone from 'backbone';
import Duration from 'duration';
import Environment from './environment';
import Flow from '../models/flow';
import FlowModel from '../models/flow';
import NotifierMixin from './channels/notifier-mixin';
import speedTrap from 'speed-trap';
import Strings from './strings';
import SubscriptionModel from 'models/subscription';
import xhr from './xhr';

// Speed trap is a singleton, convert it
@@ -53,8 +54,8 @@ const ALLOWED_FIELDS = [
'migration',
'navigationTiming',
'numStoredAccounts',
'plan_id',
'product_id',
'planId',
'productId',
'reason',
'referrer',
'screen',
@@ -153,8 +154,6 @@ function Metrics(options = {}) {
this._marketingImpressions = {};
this._migration = options.migration || NOT_REPORTED_VALUE;
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._screenHeight = options.screenHeight || NOT_REPORTED_VALUE;
this._screenWidth = options.screenWidth || NOT_REPORTED_VALUE;
@@ -190,6 +189,8 @@ _.extend(Metrics.prototype, Backbone.Events, {

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

this._initializeSubscriptionModel();
},

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

const flowModel = new Flow({
const flowModel = new FlowModel({
metrics: this,
sentryMetrics: this._sentryMetrics,
window: this._window,
@@ -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
* Log a flow event. If there is no flow model, do nothing.
@@ -383,8 +404,10 @@ _.extend(Metrics.prototype, Backbone.Events, {
marketing: flattenHashIntoArrayOfObjects(this._marketingImpressions),
migration: this._migration,
numStoredAccounts: this._numStoredAccounts,
plan_id: this._planId,
product_id: this._productId,
// planId and productId are optional so we can physically remove
// 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,
screen: {
clientHeight: this._clientHeight,
@@ -698,10 +721,14 @@ _.extend(Metrics.prototype, Backbone.Events, {
};
},

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

getSubscriptionModel() {
return this._subscriptionModel;
},

/**
* Log the number of stored accounts
*
@@ -42,4 +42,13 @@ export default {
getHashParams(paramNames) {
return Url.hashParams(this.window.location.hash, paramNames);
},

/**
* return the pathname of the window
*
* @returns {String}
*/
getPathname() {
return this.window.location.pathname;
},
};
@@ -20,6 +20,8 @@ const ALLOWED_KEYS = [
'flowId',
'needsOptedInToMarketingEmail',
'newsletters',
'planId',
'productId',
'resetPasswordConfirm',
'style',
'uniqueUserId',
@@ -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;
@@ -26,12 +26,15 @@ export default {
flowInfo = flowModel.pickResumeTokenInfo();
}

const subscriptionModel = this.metrics.getSubscriptionModel();

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

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

this.supportForm.set({

0 comments on commit ed501fa

Please sign in to comment.
You can’t perform that action at this time.