From b77c762358364695db15a738c478981bd02f086b Mon Sep 17 00:00:00 2001 From: George Gevoian Date: Tue, 23 Jan 2024 20:12:46 -0800 Subject: [PATCH] (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 --- app/client/components/DetailView.js | 2 ++ app/client/ui/DocMenu.ts | 3 ++- app/client/ui/ShareMenu.ts | 8 +++++++- app/common/Telemetry.ts | 10 +++++++++- test/gen-server/ApiServerAccess.ts | 13 ++++++++----- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/client/components/DetailView.js b/app/client/components/DetailView.js index 138a6517..5a586af1 100644 --- a/app/client/components/DetailView.js +++ b/app/client/components/DetailView.js @@ -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) ) ); diff --git a/app/client/ui/DocMenu.ts b/app/client/ui/DocMenu.ts index 6a716765..d8082572 100644 --- a/app/client/ui/DocMenu.ts +++ b/app/client/ui/DocMenu.ts @@ -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, }); diff --git a/app/client/ui/ShareMenu.ts b/app/client/ui/ShareMenu.ts index 7386523f..c9817b3d 100644 --- a/app/client/ui/ShareMenu.ts +++ b/app/client/ui/ShareMenu.ts @@ -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; diff --git a/app/common/Telemetry.ts b/app/common/Telemetry.ts index ce696078..cdae7af8 100644 --- a/app/common/Telemetry.ts +++ b/app/common/Telemetry.ts @@ -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', diff --git a/test/gen-server/ApiServerAccess.ts b/test/gen-server/ApiServerAccess.ts index 3cac5617..987b3880 100644 --- a/test/gen-server/ApiServerAccess.ts +++ b/test/gen-server/ApiServerAccess.ts @@ -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);