From ad1b4f3cff416eb6ce07cbfbdd55e1bc61df864b Mon Sep 17 00:00:00 2001 From: George Gevoian Date: Fri, 11 Mar 2022 12:35:29 -0800 Subject: [PATCH] (core) Record new user sign-ups Summary: Adds Google Tag Manager snippet to all login pages, and a new user preference, recordSignUpEvent, that's set to true on first sign-in. The client now checks for this preference, and if true, dynamically loads Google Tag Manager to record a sign-up event. Afterwards, it removes the preference. Test Plan: Tested manually. Reviewers: dsagal Reviewed By: dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3319 --- app/client/models/AppModel.ts | 34 +++++++++++++++++++++++++++++- app/client/models/gristUrlState.ts | 5 ++++- app/common/Prefs.ts | 3 +++ app/common/gristUrls.ts | 3 +++ app/common/tagManager.ts | 29 +++++++++++++++++++++++++ app/server/lib/FlexServer.ts | 15 +++++++------ app/server/lib/sendAppPage.ts | 26 +++-------------------- 7 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 app/common/tagManager.ts diff --git a/app/client/models/AppModel.ts b/app/client/models/AppModel.ts index 76885753..e3512bea 100644 --- a/app/client/models/AppModel.ts +++ b/app/client/models/AppModel.ts @@ -1,4 +1,5 @@ import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals'; +import {error} from 'app/client/lib/log'; import {reportError, setErrorNotifier} from 'app/client/models/errors'; import {urlState} from 'app/client/models/gristUrlState'; import {Notifier} from 'app/client/models/NotifyModel'; @@ -8,8 +9,10 @@ import {GristLoadConfig} from 'app/common/gristUrls'; import {FullUser} from 'app/common/LoginSessionAPI'; import {LocalPlugin} from 'app/common/plugin'; import {UserPrefs} from 'app/common/Prefs'; +import {getTagManagerScript} from 'app/common/tagManager'; +import {getGristConfig} from 'app/common/urlUtils'; import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI'; -import {getUserPrefsObs} from 'app/client/models/UserPrefs'; +import {getUserPrefObs, getUserPrefsObs} from 'app/client/models/UserPrefs'; import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs'; export {reportError} from 'app/client/models/errors'; @@ -195,6 +198,35 @@ export class AppModelImpl extends Disposable implements AppModel { public readonly orgError?: OrgError, ) { super(); + this._recordSignUpIfIsNewUser(); + } + + /** + * If the current user is a new user, record a sign-up event via Google Tag Manager. + */ + private _recordSignUpIfIsNewUser() { + const isNewUser = this.userPrefsObs.get().recordSignUpEvent; + if (!isNewUser) { return; } + + // If Google Tag Manager isn't configured, don't record anything. + const {tagManagerId} = getGristConfig(); + if (!tagManagerId) { return; } + + let dataLayer = (window as any).dataLayer; + if (!dataLayer) { + // Load the Google Tag Manager script into the document. + const script = document.createElement('script'); + script.innerHTML = getTagManagerScript(tagManagerId); + document.head.appendChild(script); + dataLayer = (window as any).dataLayer; + if (!dataLayer) { + error(`_recordSignUpIfIsNewUser() failed to load Google Tag Manager`); + } + } + + // Send the sign-up event, and remove the recordSignUpEvent flag from preferences. + dataLayer.push({event: 'new-sign-up'}); + getUserPrefObs(this.userPrefsObs, 'recordSignUpEvent').set(undefined); } } diff --git a/app/client/models/gristUrlState.ts b/app/client/models/gristUrlState.ts index 1393e54d..7a109289 100644 --- a/app/client/models/gristUrlState.ts +++ b/app/client/models/gristUrlState.ts @@ -170,8 +170,11 @@ export class UrlStateImpl { const welcomeReload = Boolean(prevState.welcome) !== Boolean(newState.welcome); // Reload when link keys change, which changes what the user can access const linkKeysReload = !isEqual(prevState.params?.linkParameters, newState.params?.linkParameters); + // Reload when moving to/from the Grist sign-up page. + const signupReload = [prevState.login, newState.login].includes('signup') + && prevState.login !== newState.login; return Boolean(orgReload || accountReload || billingReload || gristConfig.errPage - || docReload || welcomeReload || linkKeysReload); + || docReload || welcomeReload || linkKeysReload || signupReload); } /** diff --git a/app/common/Prefs.ts b/app/common/Prefs.ts index a035916b..c1d6ee8b 100644 --- a/app/common/Prefs.ts +++ b/app/common/Prefs.ts @@ -17,6 +17,9 @@ export interface UserPrefs extends Prefs { // Whether to ask the user to fill out a form about their use-case, on opening the DocMenu page. // Set to true on first login, then reset when the form is closed, so that it only shows once. showNewUserQuestions?: boolean; + // Whether to record a new sign-up event via Google Tag Manager. Set to true on first login, then + // reset on first page load (after the event is sent), so that it's only recorded once. + recordSignUpEvent?: boolean; } export interface UserOrgPrefs extends Prefs { diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index c29cc20a..3c7d29ee 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -496,6 +496,9 @@ export interface GristLoadConfig { // Whether there is somewhere for survey data to go. survey?: boolean; + + // Google Tag Manager id. Currently only used to load tag manager for reporting new sign-ups. + tagManagerId?: string; } // Acceptable org subdomains are alphanumeric (hyphen also allowed) and of diff --git a/app/common/tagManager.ts b/app/common/tagManager.ts new file mode 100644 index 00000000..4e2b7427 --- /dev/null +++ b/app/common/tagManager.ts @@ -0,0 +1,29 @@ +/** + * Returns the Google Tag Manager snippet to insert into of the page, if + * `tagId` is set to a non-empty value. Otherwise returns an empty string. + */ +export function getTagManagerSnippet(tagId?: string) { + // Note also that we only insert the snippet for the . The second recommended part (for + // ) is for