(core) Add sign-up and sharing/invite telemetry

Summary:
Enhances sign-up telemetry with login and verification method metadata, and
adds UTM parameters to SendGrid invite email links and Grist document links.

Test Plan: Server and manual.

Reviewers: dsagal

Reviewed By: dsagal

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D4169
This commit is contained in:
George Gevoian 2024-01-23 20:12:46 -08:00
parent dba3a59486
commit b77c762358
5 changed files with 28 additions and 8 deletions

View File

@ -311,6 +311,8 @@ DetailView.prototype.buildFieldDom = function(field, row) {
kd.toggleClass('scissors', isCopyActive),
kd.toggleClass('record-add', row._isAddRow),
dom.autoDispose(isCopyActive),
// Optional icon. Currently only use to show formula icon.
dom('div.field-icon'),
fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected)
)
);

View File

@ -15,6 +15,7 @@ import {buildUpgradeButton} from 'app/client/ui/ProductUpgrades';
import {buildTutorialCard} from 'app/client/ui/TutorialCard';
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
import {shadowScroll} from 'app/client/ui/shadowScroll';
import {makeShareDocUrl} from 'app/client/ui/ShareMenu';
import {transition} from 'app/client/ui/transitions';
import {shouldShowWelcomeCoachingCall, showWelcomeCoachingCall} from 'app/client/ui/WelcomeCoachingCall';
import {shouldShowWelcomeQuestions, showWelcomeQuestions} from 'app/client/ui/WelcomeQuestions';
@ -496,7 +497,7 @@ export function makeDocOptionsMenu(home: HomeModel, doc: Document, renaming: Obs
resourceType: 'document',
resourceId: doc.id,
resource: doc,
linkToCopy: urlState().makeUrl(docUrl(doc)),
linkToCopy: makeShareDocUrl(doc),
reload: () => api.getDocAccess(doc.id),
appModel: home.app,
});

View File

@ -305,7 +305,7 @@ async function manageUsers(doc: DocInfo, docPageModel: DocPageModel) {
resource: doc,
docPageModel,
appModel: docPageModel.appModel,
linkToCopy: urlState().makeUrl(docUrl(doc)),
linkToCopy: makeShareDocUrl(doc),
// On save, re-fetch the document info, to toggle the "Public Access" icon if it changed.
// Skip if personal, since personal cannot affect "Public Access", and the only
// change possible is to remove the user (which would make refreshCurrentDoc fail)
@ -314,6 +314,12 @@ async function manageUsers(doc: DocInfo, docPageModel: DocPageModel) {
});
}
export function makeShareDocUrl(doc: Document) {
const url = new URL(urlState().makeUrl(docUrl(doc)));
url.searchParams.set('utm_id', 'share-doc');
return url.href;
}
const cssShareButton = styled('div', `
display: flex;
align-items: center;

View File

@ -770,10 +770,14 @@ export const TelemetryContracts: TelemetryContracts = {
},
signupFirstVisit: {
category: 'ProductVisits',
description: 'Triggered when a new user first opens the Grist app',
description: 'Triggered when a new user first opens the Grist app.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
loginMethod: {
description: 'The login method on getgrist.com. May be "Email + Password" or "Google".',
dataType: 'string',
},
siteId: {
description: 'The site id of first visit after signup.',
dataType: 'number',
@ -798,6 +802,10 @@ export const TelemetryContracts: TelemetryContracts = {
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
verificationMethod: {
description: 'The verification method. May be "code" or "link".',
dataType: 'string',
},
isAnonymousTemplateSignup: {
description: 'Whether the user viewed any templates before signing up.',
dataType: 'boolean',

View File

@ -144,7 +144,7 @@ describe('ApiServerAccess', function() {
// We should send mail about this one, since Kiwi had no access previously.
if (notificationsConfig) {
const mail = await assertLastMail();
assert.match(mail.description, /^invite kiwi@getgrist.com to http.*\/o\/docs\/$/);
assert.match(mail.description, /^invite kiwi@getgrist.com to http.*\/o\/docs\/\?utm_id=invite-org$/);
const env = mail.payload.personalizations[0].dynamic_template_data;
assert.deepEqual(pick(env, ['resource.name', 'resource.kind', 'resource.kindUpperFirst',
'resource.isTeamSite', 'resource.isWorkspace', 'resource.isDocument',
@ -159,7 +159,7 @@ describe('ApiServerAccess', function() {
user: {name: 'Kiwi', email: 'kiwi@getgrist.com'},
access: {role: 'editors', canEdit: true, canView: true}
} as any);
assert.match(env.resource.url, /^http.*\/o\/docs\/$/);
assert.match(env.resource.url, /^http.*\/o\/docs\/\?utm_id=invite-org$/);
assert.deepEqual(mail.payload.personalizations[0].to[0], {email: 'kiwi@getgrist.com', name: 'Kiwi'});
assert.deepEqual(mail.payload.from, {email: 'support@getgrist.com', name: 'Chimpy (via Grist)'});
assert.deepEqual(mail.payload.reply_to, {email: 'chimpy@getgrist.com', name: 'Chimpy'});
@ -207,7 +207,10 @@ describe('ApiServerAccess', function() {
const resp4 = await axios.patch(`${homeUrl}/api/orgs/${nasaOrgId}/access`, {delta: delta4}, chimpy);
assert.equal(resp4.status, 200);
if (notificationsConfig) {
assert.match((await assertLastMail()).description, /^invite kiwi@getgrist.com to http.*\/o\/nasa\/$/);
assert.match(
(await assertLastMail()).description,
/^invite kiwi@getgrist.com to http.*\/o\/nasa\/\?utm_id=invite-org$/
);
}
// Assert that the number of users in the org has updated (Kiwi was added).
assert.deepEqual(userCountUpdates[nasaOrgId as number], [2]);
@ -349,9 +352,9 @@ describe('ApiServerAccess', function() {
// Check we would sent an email to Kiwi about this
if (notificationsConfig) {
const mail = await assertLastMail();
assert.match(mail.description, /^invite kiwi@getgrist.com to http.*\/o\/docs\/ws\/[0-9]+\/$/);
assert.match(mail.description, /^invite kiwi@getgrist.com to http.*\/o\/docs\/ws\/[0-9]+\/\?utm_id=invite-ws$/);
const env = mail.payload.personalizations[0].dynamic_template_data;
assert.match(env.resource.url, /^http.*\/o\/docs\/ws\/[0-9]+\/$/);
assert.match(env.resource.url, /^http.*\/o\/docs\/ws\/[0-9]+\/\?utm_id=invite-ws$/);
assert.equal(env.resource.kind, 'workspace');
assert.equal(env.resource.kindUpperFirst, 'Workspace');
assert.equal(env.resource.isTeamSite, false);