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")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-click="startFreeTrial('real-time-track-changes')"
|
||||
ng-click="startFreeTrial('track-changes')"
|
||||
ng-show="project.owner._id == user.id"
|
||||
) #{translate("try_it_for_free")}
|
||||
div(ng-show="!user.allowedFreeTrial" ng-controller="UpgradeModalController")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-click="upgradePlan('projectMembers')"
|
||||
ng-click="upgradePlan('project-sharing')"
|
||||
ng-show="project.owner._id == user.id"
|
||||
) #{translate("upgrade")}
|
||||
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
|
||||
buttonStyle="success"
|
||||
setStartedFreeTrial={setStartedFreeTrial}
|
||||
source="projectMembers"
|
||||
source="project-sharing"
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
bsStyle="success"
|
||||
onClick={() => {
|
||||
upgradePlan('projectMembers')
|
||||
upgradePlan('project-sharing')
|
||||
setStartedFreeTrial(true)
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -35,6 +35,7 @@ import './components/historyLabelsList'
|
|||
import './components/historyLabel'
|
||||
import './components/historyFileTree'
|
||||
import './components/historyFileEntity'
|
||||
import { paywallPrompt } from '../../../../frontend/js/main/account-upgrade'
|
||||
let HistoryManager
|
||||
|
||||
export default HistoryManager = (function () {
|
||||
|
@ -939,6 +940,7 @@ export default HistoryManager = (function () {
|
|||
'editor-click-feature',
|
||||
'history'
|
||||
)
|
||||
paywallPrompt('history')
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
|
@ -441,6 +441,7 @@ App.controller(
|
|||
'editor-click-feature',
|
||||
'compile-timeout'
|
||||
)
|
||||
eventTracking.sendMB('compile-timeout-paywall-prompt')
|
||||
}
|
||||
} else if (response.status === 'terminated') {
|
||||
$scope.pdf.view = 'errors'
|
||||
|
|
|
@ -917,6 +917,7 @@ export default App.controller(
|
|||
'editor-click-feature',
|
||||
'real-time-track-changes'
|
||||
)
|
||||
eventTracking.sendMB('track-changes-paywall-prompt')
|
||||
}
|
||||
|
||||
const _setUserTCState = function (userId, newValue, isLocal) {
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { postJSON } from './fetch-json'
|
||||
import sessionStorage from '../infrastructure/session-storage'
|
||||
|
||||
const CACHE_KEY = 'mbEvents'
|
||||
|
||||
export function send(category, action, label, value) {
|
||||
if (typeof window.ga === 'function') {
|
||||
|
@ -6,10 +9,29 @@ export function send(category, action, label, value) {
|
|||
}
|
||||
}
|
||||
|
||||
export function sendMB(key, body = {}) {
|
||||
postJSON(`/event/${key}`, { body, keepalive: true }).catch(() => {
|
||||
// ignore errors
|
||||
})
|
||||
export function sendMB(key, segmentation = {}) {
|
||||
postJSON(`/event/${key}`, { body: segmentation, keepalive: true }).catch(
|
||||
() => {
|
||||
// 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) {
|
||||
|
|
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 { 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.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.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 w = window.open()
|
||||
|
@ -7,6 +9,8 @@ function startFreeTrial(source, version, $scope, eventTracking) {
|
|||
if (typeof ga === 'function') {
|
||||
ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
|
||||
}
|
||||
eventTracking.sendMB(`${source}-paywall-click`)
|
||||
|
||||
url = `/user/subscription/new?planCode=${plan}&ssp=true`
|
||||
url = `${url}&itm_campaign=${source}`
|
||||
if (version) {
|
||||
|
@ -17,10 +21,6 @@ function startFreeTrial(source, version, $scope, eventTracking) {
|
|||
$scope.startedFreeTrial = true
|
||||
}
|
||||
|
||||
if (eventTracking) {
|
||||
eventTracking.sendMB('subscription-start-trial', { source, plan })
|
||||
}
|
||||
|
||||
w.location = url
|
||||
}
|
||||
|
||||
|
@ -45,4 +45,8 @@ function upgradePlan(source, $scope) {
|
|||
go()
|
||||
}
|
||||
|
||||
export { startFreeTrial, upgradePlan }
|
||||
function paywallPrompt(source) {
|
||||
eventTracking.sendMB(`${source}-paywall-prompt`)
|
||||
}
|
||||
|
||||
export { startFreeTrial, upgradePlan, paywallPrompt }
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
angular
|
||||
.module('sessionStorage', [])
|
||||
.value('sessionStorage', function (...args) {
|
||||
/*
|
||||
sessionStorage can throw browser exceptions, for example if it is full
|
||||
We don't use sessionStorage for anything critical, on in that case just
|
||||
fail gracefully.
|
||||
*/
|
||||
try {
|
||||
return $.sessionStorage(...args)
|
||||
} catch (e) {
|
||||
console.error('sessionStorage exception', e)
|
||||
return null
|
||||
}
|
||||
})
|
||||
angular.module('sessionStorage', []).value('sessionStorage', sessionStorage)
|
||||
|
||||
/*
|
||||
sessionStorage can throw browser exceptions, for example if it is full
|
||||
We don't use sessionStorage for anything critical, on in that case just
|
||||
fail gracefully.
|
||||
*/
|
||||
function sessionStorage(...args) {
|
||||
try {
|
||||
return $.sessionStorage(...args)
|
||||
} catch (e) {
|
||||
console.error('sessionStorage exception', e)
|
||||
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 { Button } from 'react-bootstrap'
|
||||
import PropTypes from 'prop-types'
|
||||
|
@ -13,25 +13,23 @@ export default function StartFreeTrialButton({
|
|||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
eventTracking.sendMB(`${source}-paywall-prompt`)
|
||||
}, [source])
|
||||
|
||||
const handleClick = useCallback(
|
||||
event => {
|
||||
event.preventDefault()
|
||||
|
||||
eventTracking.send('subscription-funnel', 'upgraded-free-trial', source)
|
||||
|
||||
const planCode = 'collaborator_free_trial_7_days'
|
||||
|
||||
eventTracking.sendMB('subscription-start-trial', {
|
||||
source,
|
||||
plan: planCode,
|
||||
})
|
||||
eventTracking.sendMB(`${source}-paywall-click`)
|
||||
|
||||
if (setStartedFreeTrial) {
|
||||
setStartedFreeTrial(true)
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
planCode,
|
||||
planCode: 'collaborator_free_trial_7_days',
|
||||
ssp: 'true',
|
||||
itm_campaign: source,
|
||||
})
|
||||
|
|
|
@ -639,6 +639,8 @@ describe('<ShareProjectModal/>', function () {
|
|||
})
|
||||
|
||||
it('displays a message when the collaborator limit is reached', async function () {
|
||||
fetchMock.post('/event/project-sharing-paywall-prompt', {})
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal
|
||||
{...modalProps}
|
||||
|
|
|
@ -157,11 +157,15 @@ export default describe('HistoryV2Manager', function () {
|
|||
$http: $http,
|
||||
$filter: $filter,
|
||||
}
|
||||
this.eventTracking = {
|
||||
sendMB: () => {},
|
||||
}
|
||||
this.localStorage = sinon.stub().returns(null)
|
||||
this.historyManager = new HistoryV2Manager(
|
||||
this.ide,
|
||||
this.$scope,
|
||||
this.localStorage
|
||||
this.localStorage,
|
||||
this.eventTracking
|
||||
)
|
||||
done()
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue