mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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
This commit is contained in:
parent
eff78ae2e1
commit
ad1b4f3cff
@ -1,4 +1,5 @@
|
|||||||
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
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 {reportError, setErrorNotifier} from 'app/client/models/errors';
|
||||||
import {urlState} from 'app/client/models/gristUrlState';
|
import {urlState} from 'app/client/models/gristUrlState';
|
||||||
import {Notifier} from 'app/client/models/NotifyModel';
|
import {Notifier} from 'app/client/models/NotifyModel';
|
||||||
@ -8,8 +9,10 @@ import {GristLoadConfig} from 'app/common/gristUrls';
|
|||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
import {LocalPlugin} from 'app/common/plugin';
|
import {LocalPlugin} from 'app/common/plugin';
|
||||||
import {UserPrefs} from 'app/common/Prefs';
|
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 {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';
|
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||||
|
|
||||||
export {reportError} from 'app/client/models/errors';
|
export {reportError} from 'app/client/models/errors';
|
||||||
@ -195,6 +198,35 @@ export class AppModelImpl extends Disposable implements AppModel {
|
|||||||
public readonly orgError?: OrgError,
|
public readonly orgError?: OrgError,
|
||||||
) {
|
) {
|
||||||
super();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,8 +170,11 @@ export class UrlStateImpl {
|
|||||||
const welcomeReload = Boolean(prevState.welcome) !== Boolean(newState.welcome);
|
const welcomeReload = Boolean(prevState.welcome) !== Boolean(newState.welcome);
|
||||||
// Reload when link keys change, which changes what the user can access
|
// Reload when link keys change, which changes what the user can access
|
||||||
const linkKeysReload = !isEqual(prevState.params?.linkParameters, newState.params?.linkParameters);
|
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
|
return Boolean(orgReload || accountReload || billingReload || gristConfig.errPage
|
||||||
|| docReload || welcomeReload || linkKeysReload);
|
|| docReload || welcomeReload || linkKeysReload || signupReload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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.
|
// 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.
|
// Set to true on first login, then reset when the form is closed, so that it only shows once.
|
||||||
showNewUserQuestions?: boolean;
|
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 {
|
export interface UserOrgPrefs extends Prefs {
|
||||||
|
@ -496,6 +496,9 @@ export interface GristLoadConfig {
|
|||||||
|
|
||||||
// Whether there is somewhere for survey data to go.
|
// Whether there is somewhere for survey data to go.
|
||||||
survey?: boolean;
|
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
|
// Acceptable org subdomains are alphanumeric (hyphen also allowed) and of
|
||||||
|
29
app/common/tagManager.ts
Normal file
29
app/common/tagManager.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Returns the Google Tag Manager snippet to insert into <head> 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 <head>. The second recommended part (for
|
||||||
|
// <body>) is for <noscript> scenario, which doesn't apply to the Grist app (such visits, if
|
||||||
|
// any, wouldn't work and shouldn't be counted for any metrics we care about).
|
||||||
|
if (!tagId) { return ""; }
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!-- Google Tag Manager -->
|
||||||
|
<script>${getTagManagerScript(tagId)}</script>
|
||||||
|
<!-- End Google Tag Manager -->
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the body of the Google Tag Manager script. This is suitable for use by the client,
|
||||||
|
* since it must dynamically load it by calling `document.createElement('script')` and setting
|
||||||
|
* its `innerHTML`.
|
||||||
|
*/
|
||||||
|
export function getTagManagerScript(tagId: string) {
|
||||||
|
return `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||||
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||||
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||||
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||||
|
})(window,document,'script','dataLayer','${tagId}');`;
|
||||||
|
}
|
@ -722,11 +722,14 @@ export class FlexServer implements GristServer {
|
|||||||
// Reset isFirstTimeUser flag.
|
// Reset isFirstTimeUser flag.
|
||||||
await this._dbManager.updateUser(user.id, {isFirstTimeUser: false});
|
await this._dbManager.updateUser(user.id, {isFirstTimeUser: false});
|
||||||
|
|
||||||
// This is a good time to set another flag (showNewUserQuestions), to show a popup with
|
// This is a good time to set some other flags, for showing a popup with welcome question(s)
|
||||||
// welcome question(s) to this new user. Both flags are scoped to the user, but
|
// to this new user and recording their sign-up with Google Tag Manager. These flags are also
|
||||||
// isFirstTimeUser has a dedicated DB field because it predates userPrefs. Note that the
|
// scoped to the user, but isFirstTimeUser has a dedicated DB field because it predates userPrefs.
|
||||||
// updateOrg() method handles all levels of prefs (for user, user+org, or org).
|
// Note that the updateOrg() method handles all levels of prefs (for user, user+org, or org).
|
||||||
await this._dbManager.updateOrg(getScope(req), 0, {userPrefs: {showNewUserQuestions: true}});
|
await this._dbManager.updateOrg(getScope(req), 0, {userPrefs: {
|
||||||
|
showNewUserQuestions: true,
|
||||||
|
recordSignUpEvent: true,
|
||||||
|
}});
|
||||||
|
|
||||||
// Redirect to teams page if users has access to more than one org. Otherwise, redirect to
|
// Redirect to teams page if users has access to more than one org. Otherwise, redirect to
|
||||||
// personal org.
|
// personal org.
|
||||||
@ -1138,7 +1141,7 @@ export class FlexServer implements GristServer {
|
|||||||
|
|
||||||
// These are some special-purpose welcome pages, with no middleware.
|
// These are some special-purpose welcome pages, with no middleware.
|
||||||
this.app.get(/\/welcome\/(signup|verify|teams|select-account)/, expressWrap(async (req, resp, next) => {
|
this.app.get(/\/welcome\/(signup|verify|teams|select-account)/, expressWrap(async (req, resp, next) => {
|
||||||
return this._sendAppPage(req, resp, {path: 'app.html', status: 200, config: {}, googleTagManager: true});
|
return this._sendAppPage(req, resp, {path: 'app.html', status: 200, config: {}, googleTagManager: 'anon'});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.app.post('/welcome/info', ...middleware, expressWrap(async (req, resp, next) => {
|
this.app.post('/welcome/info', ...middleware, expressWrap(async (req, resp, next) => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {GristLoadConfig} from 'app/common/gristUrls';
|
import {GristLoadConfig} from 'app/common/gristUrls';
|
||||||
|
import {getTagManagerSnippet} from 'app/common/tagManager';
|
||||||
import {isAnonymousUser} from 'app/server/lib/Authorizer';
|
import {isAnonymousUser} from 'app/server/lib/Authorizer';
|
||||||
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
||||||
import {GristServer} from 'app/server/lib/GristServer';
|
import {GristServer} from 'app/server/lib/GristServer';
|
||||||
@ -47,6 +48,7 @@ export function makeGristConfig(homeUrl: string|null, extra: Partial<GristLoadCo
|
|||||||
timestampMs: Date.now(),
|
timestampMs: Date.now(),
|
||||||
enableWidgetRepository: Boolean(process.env.GRIST_WIDGET_LIST_URL),
|
enableWidgetRepository: Boolean(process.env.GRIST_WIDGET_LIST_URL),
|
||||||
survey: Boolean(process.env.DOC_ID_NEW_USER_INFO),
|
survey: Boolean(process.env.DOC_ID_NEW_USER_INFO),
|
||||||
|
tagManagerId: process.env.GOOGLE_TAG_MANAGER_ID,
|
||||||
...extra,
|
...extra,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -85,7 +87,7 @@ export function makeSendAppPage(opts: {
|
|||||||
|
|
||||||
const needTagManager = (options.googleTagManager === 'anon' && isAnonymousUser(req)) ||
|
const needTagManager = (options.googleTagManager === 'anon' && isAnonymousUser(req)) ||
|
||||||
options.googleTagManager === true;
|
options.googleTagManager === true;
|
||||||
const tagManagerSnippet = needTagManager ? getTagManagerSnippet() : '';
|
const tagManagerSnippet = needTagManager ? getTagManagerSnippet(process.env.GOOGLE_TAG_MANAGER_ID) : '';
|
||||||
const staticOrigin = process.env.APP_STATIC_URL || "";
|
const staticOrigin = process.env.APP_STATIC_URL || "";
|
||||||
const staticBaseUrl = `${staticOrigin}/v/${options.tag || tag}/`;
|
const staticBaseUrl = `${staticOrigin}/v/${options.tag || tag}/`;
|
||||||
const warning = testLogin ? "<div class=\"dev_warning\">Authentication is not enforced</div>" : "";
|
const warning = testLogin ? "<div class=\"dev_warning\">Authentication is not enforced</div>" : "";
|
||||||
@ -101,25 +103,3 @@ function shouldSupportAnon() {
|
|||||||
// Enable UI for anonymous access if a flag is explicitly set in the environment
|
// Enable UI for anonymous access if a flag is explicitly set in the environment
|
||||||
return process.env.GRIST_SUPPORT_ANON === "true";
|
return process.env.GRIST_SUPPORT_ANON === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Google Tag Manager snippet to insert into <head> of the page, if
|
|
||||||
* GOOGLE_TAG_MANAGER_ID env var is set to a non-empty value. Otherwise returns the empty string.
|
|
||||||
*/
|
|
||||||
function getTagManagerSnippet() {
|
|
||||||
// Note also that we only insert the snippet for the <head>. The second recommended part (for
|
|
||||||
// <body>) is for <noscript> scenario, which doesn't apply to the Grist app (such visits, if
|
|
||||||
// any, wouldn't work and shouldn't be counted for any metrics we care about).
|
|
||||||
const tagId = process.env.GOOGLE_TAG_MANAGER_ID;
|
|
||||||
if (!tagId) { return ""; }
|
|
||||||
|
|
||||||
return `
|
|
||||||
<!-- Google Tag Manager -->
|
|
||||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
|
||||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
||||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
||||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
||||||
})(window,document,'script','dataLayer','${tagId}');</script>
|
|
||||||
<!-- End Google Tag Manager -->
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user