Merge pull request #4116 from overleaf/ab-paywall-prompt-events

Add paywall prompt events

GitOrigin-RevId: 6b1b3b384590f14828f37210b2e14047e2ee33d6
This commit is contained in:
Alexandre Bourdin 2021-06-10 10:04:21 +02:00 committed by Copybot
parent e2d116e8be
commit c634f51eee
13 changed files with 125 additions and 43 deletions

View file

@ -583,13 +583,13 @@ script(type="text/ng-template", id="trackChangesUpgradeModalTemplate")
div(ng-show="user.allowedFreeTrial" ng-controller="FreeTrialModalController") div(ng-show="user.allowedFreeTrial" ng-controller="FreeTrialModalController")
a.btn.btn-success( a.btn.btn-success(
href href
ng-click="startFreeTrial('real-time-track-changes')" ng-click="startFreeTrial('track-changes')"
ng-show="project.owner._id == user.id" ng-show="project.owner._id == user.id"
) #{translate("try_it_for_free")} ) #{translate("try_it_for_free")}
div(ng-show="!user.allowedFreeTrial" ng-controller="UpgradeModalController") div(ng-show="!user.allowedFreeTrial" ng-controller="UpgradeModalController")
a.btn.btn-success( a.btn.btn-success(
href href
ng-click="upgradePlan('projectMembers')" ng-click="upgradePlan('project-sharing')"
ng-show="project.owner._id == user.id" ng-show="project.owner._id == user.id"
) #{translate("upgrade")} ) #{translate("upgrade")}
p(ng-show="project.owner._id != user.id"): strong #{translate("please_ask_the_project_owner_to_upgrade_to_track_changes")} p(ng-show="project.owner._id != user.id"): strong #{translate("please_ask_the_project_owner_to_upgrade_to_track_changes")}

View file

@ -64,13 +64,13 @@ export default function AddCollaboratorsUpgrade() {
<StartFreeTrialButton <StartFreeTrialButton
buttonStyle="success" buttonStyle="success"
setStartedFreeTrial={setStartedFreeTrial} setStartedFreeTrial={setStartedFreeTrial}
source="projectMembers" source="project-sharing"
/> />
) : ( ) : (
<Button <Button
bsStyle="success" bsStyle="success"
onClick={() => { onClick={() => {
upgradePlan('projectMembers') upgradePlan('project-sharing')
setStartedFreeTrial(true) setStartedFreeTrial(true)
}} }}
> >

View file

@ -35,6 +35,7 @@ import './components/historyLabelsList'
import './components/historyLabel' import './components/historyLabel'
import './components/historyFileTree' import './components/historyFileTree'
import './components/historyFileEntity' import './components/historyFileEntity'
import { paywallPrompt } from '../../../../frontend/js/main/account-upgrade'
let HistoryManager let HistoryManager
export default HistoryManager = (function () { export default HistoryManager = (function () {
@ -939,6 +940,7 @@ export default HistoryManager = (function () {
'editor-click-feature', 'editor-click-feature',
'history' 'history'
) )
paywallPrompt('history')
} }
break break
} }

View file

@ -441,6 +441,7 @@ App.controller(
'editor-click-feature', 'editor-click-feature',
'compile-timeout' 'compile-timeout'
) )
eventTracking.sendMB('compile-timeout-paywall-prompt')
} }
} else if (response.status === 'terminated') { } else if (response.status === 'terminated') {
$scope.pdf.view = 'errors' $scope.pdf.view = 'errors'

View file

@ -917,6 +917,7 @@ export default App.controller(
'editor-click-feature', 'editor-click-feature',
'real-time-track-changes' 'real-time-track-changes'
) )
eventTracking.sendMB('track-changes-paywall-prompt')
} }
const _setUserTCState = function (userId, newValue, isLocal) { const _setUserTCState = function (userId, newValue, isLocal) {

View file

@ -1,4 +1,7 @@
import { postJSON } from './fetch-json' import { postJSON } from './fetch-json'
import sessionStorage from '../infrastructure/session-storage'
const CACHE_KEY = 'mbEvents'
export function send(category, action, label, value) { export function send(category, action, label, value) {
if (typeof window.ga === 'function') { if (typeof window.ga === 'function') {
@ -6,10 +9,29 @@ export function send(category, action, label, value) {
} }
} }
export function sendMB(key, body = {}) { export function sendMB(key, segmentation = {}) {
postJSON(`/event/${key}`, { body, keepalive: true }).catch(() => { postJSON(`/event/${key}`, { body: segmentation, keepalive: true }).catch(
// ignore errors () => {
}) // ignore errors
}
)
}
export function sendMBOnce(key, segmentation = {}) {
let eventCache = sessionStorage.getItem(CACHE_KEY)
// Initialize as an empy object if the event cache is still empty.
if (eventCache == null) {
eventCache = {}
sessionStorage.setItem(CACHE_KEY, eventCache)
}
const isEventInCache = eventCache[key] || false
if (!isEventInCache) {
eventCache[key] = true
sessionStorage.setItem(CACHE_KEY, eventCache)
sendMB(key, segmentation)
}
} }
export function sendMBSampled(key, body = {}, rate = 0.01) { export function sendMBSampled(key, body = {}, rate = 0.01) {

View file

@ -0,0 +1,45 @@
/**
* sessionStorage can throw browser exceptions, for example if it is full.
* We don't use sessionStorage for anything critical, so in that case just fail gracefully.
*/
/**
* Catch, log and otherwise ignore errors.
*
* @param {function} fn sessionStorage function to call
* @param {string?} key Key passed to the sessionStorage function (if any)
* @param {any?} value Value passed to the sessionStorage function (if any)
*/
const callSafe = function (fn, key, value) {
try {
return fn(key, value)
} catch (e) {
console.error('sessionStorage exception', e)
return null
}
}
const getItem = function (key) {
return JSON.parse(sessionStorage.getItem(key))
}
const setItem = function (key, value) {
sessionStorage.setItem(key, JSON.stringify(value))
}
const clear = function () {
sessionStorage.clear()
}
const removeItem = function (key) {
return sessionStorage.removeItem(key)
}
const customSessionStorage = {
getItem: key => callSafe(getItem, key),
setItem: (key, value) => callSafe(setItem, key, value),
clear: () => callSafe(clear),
removeItem: key => callSafe(removeItem, key),
}
export default customSessionStorage

View file

@ -1,13 +1,14 @@
import App from '../base' import App from '../base'
import { startFreeTrial, upgradePlan } from './account-upgrade' import { startFreeTrial, upgradePlan, paywallPrompt } from './account-upgrade'
App.controller('FreeTrialModalController', function ($scope, eventTracking) { App.controller('FreeTrialModalController', function ($scope) {
$scope.buttonClass = 'btn-primary' $scope.buttonClass = 'btn-primary'
$scope.startFreeTrial = (source, version) => $scope.startFreeTrial = (source, version) =>
startFreeTrial(source, version, $scope, eventTracking) startFreeTrial(source, version, $scope)
$scope.paywallPrompt = source => paywallPrompt(source)
}) })
App.controller('UpgradeModalController', function ($scope, eventTracking) { App.controller('UpgradeModalController', function ($scope) {
$scope.buttonClass = 'btn-primary' $scope.buttonClass = 'btn-primary'
$scope.upgradePlan = source => upgradePlan(source, $scope) $scope.upgradePlan = source => upgradePlan(source, $scope)
}) })

View file

@ -1,4 +1,6 @@
function startFreeTrial(source, version, $scope, eventTracking) { import * as eventTracking from '../infrastructure/event-tracking'
function startFreeTrial(source, version, $scope) {
const plan = 'collaborator_free_trial_7_days' const plan = 'collaborator_free_trial_7_days'
const w = window.open() const w = window.open()
@ -7,6 +9,8 @@ function startFreeTrial(source, version, $scope, eventTracking) {
if (typeof ga === 'function') { if (typeof ga === 'function') {
ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source) ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
} }
eventTracking.sendMB(`${source}-paywall-click`)
url = `/user/subscription/new?planCode=${plan}&ssp=true` url = `/user/subscription/new?planCode=${plan}&ssp=true`
url = `${url}&itm_campaign=${source}` url = `${url}&itm_campaign=${source}`
if (version) { if (version) {
@ -17,10 +21,6 @@ function startFreeTrial(source, version, $scope, eventTracking) {
$scope.startedFreeTrial = true $scope.startedFreeTrial = true
} }
if (eventTracking) {
eventTracking.sendMB('subscription-start-trial', { source, plan })
}
w.location = url w.location = url
} }
@ -45,4 +45,8 @@ function upgradePlan(source, $scope) {
go() go()
} }
export { startFreeTrial, upgradePlan } function paywallPrompt(source) {
eventTracking.sendMB(`${source}-paywall-prompt`)
}
export { startFreeTrial, upgradePlan, paywallPrompt }

View file

@ -1,15 +1,17 @@
angular angular.module('sessionStorage', []).value('sessionStorage', sessionStorage)
.module('sessionStorage', [])
.value('sessionStorage', function (...args) { /*
/* sessionStorage can throw browser exceptions, for example if it is full
sessionStorage can throw browser exceptions, for example if it is full We don't use sessionStorage for anything critical, on in that case just
We don't use sessionStorage for anything critical, on in that case just fail gracefully.
fail gracefully. */
*/ function sessionStorage(...args) {
try { try {
return $.sessionStorage(...args) return $.sessionStorage(...args)
} catch (e) { } catch (e) {
console.error('sessionStorage exception', e) console.error('sessionStorage exception', e)
return null return null
} }
}) }
export default sessionStorage

View file

@ -1,4 +1,4 @@
import React, { useCallback } from 'react' import React, { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
@ -13,25 +13,23 @@ export default function StartFreeTrialButton({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
useEffect(() => {
eventTracking.sendMB(`${source}-paywall-prompt`)
}, [source])
const handleClick = useCallback( const handleClick = useCallback(
event => { event => {
event.preventDefault() event.preventDefault()
eventTracking.send('subscription-funnel', 'upgraded-free-trial', source) eventTracking.send('subscription-funnel', 'upgraded-free-trial', source)
eventTracking.sendMB(`${source}-paywall-click`)
const planCode = 'collaborator_free_trial_7_days'
eventTracking.sendMB('subscription-start-trial', {
source,
plan: planCode,
})
if (setStartedFreeTrial) { if (setStartedFreeTrial) {
setStartedFreeTrial(true) setStartedFreeTrial(true)
} }
const params = new URLSearchParams({ const params = new URLSearchParams({
planCode, planCode: 'collaborator_free_trial_7_days',
ssp: 'true', ssp: 'true',
itm_campaign: source, itm_campaign: source,
}) })

View file

@ -639,6 +639,8 @@ describe('<ShareProjectModal/>', function () {
}) })
it('displays a message when the collaborator limit is reached', async function () { it('displays a message when the collaborator limit is reached', async function () {
fetchMock.post('/event/project-sharing-paywall-prompt', {})
renderWithEditorContext( renderWithEditorContext(
<ShareProjectModal <ShareProjectModal
{...modalProps} {...modalProps}

View file

@ -157,11 +157,15 @@ export default describe('HistoryV2Manager', function () {
$http: $http, $http: $http,
$filter: $filter, $filter: $filter,
} }
this.eventTracking = {
sendMB: () => {},
}
this.localStorage = sinon.stub().returns(null) this.localStorage = sinon.stub().returns(null)
this.historyManager = new HistoryV2Manager( this.historyManager = new HistoryV2Manager(
this.ide, this.ide,
this.$scope, this.$scope,
this.localStorage this.localStorage,
this.eventTracking
) )
done() done()
}) })