mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #4116 from overleaf/ab-paywall-prompt-events
Add paywall prompt events GitOrigin-RevId: 6b1b3b384590f14828f37210b2e14047e2ee33d6
This commit is contained in:
parent
e2d116e8be
commit
c634f51eee
13 changed files with 125 additions and 43 deletions
|
@ -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")}
|
||||||
|
|
|
@ -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)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
45
services/web/frontend/js/infrastructure/session-storage.js
Normal file
45
services/web/frontend/js/infrastructure/session-storage.js
Normal 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
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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()
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue