mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
dba3a59486
commit
b77c762358
@ -311,6 +311,8 @@ DetailView.prototype.buildFieldDom = function(field, row) {
|
|||||||
kd.toggleClass('scissors', isCopyActive),
|
kd.toggleClass('scissors', isCopyActive),
|
||||||
kd.toggleClass('record-add', row._isAddRow),
|
kd.toggleClass('record-add', row._isAddRow),
|
||||||
dom.autoDispose(isCopyActive),
|
dom.autoDispose(isCopyActive),
|
||||||
|
// Optional icon. Currently only use to show formula icon.
|
||||||
|
dom('div.field-icon'),
|
||||||
fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected)
|
fieldBuilder.buildDomWithCursor(row, isCellActive, isCellSelected)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -15,6 +15,7 @@ import {buildUpgradeButton} from 'app/client/ui/ProductUpgrades';
|
|||||||
import {buildTutorialCard} from 'app/client/ui/TutorialCard';
|
import {buildTutorialCard} from 'app/client/ui/TutorialCard';
|
||||||
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
|
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
|
||||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||||
|
import {makeShareDocUrl} from 'app/client/ui/ShareMenu';
|
||||||
import {transition} from 'app/client/ui/transitions';
|
import {transition} from 'app/client/ui/transitions';
|
||||||
import {shouldShowWelcomeCoachingCall, showWelcomeCoachingCall} from 'app/client/ui/WelcomeCoachingCall';
|
import {shouldShowWelcomeCoachingCall, showWelcomeCoachingCall} from 'app/client/ui/WelcomeCoachingCall';
|
||||||
import {shouldShowWelcomeQuestions, showWelcomeQuestions} from 'app/client/ui/WelcomeQuestions';
|
import {shouldShowWelcomeQuestions, showWelcomeQuestions} from 'app/client/ui/WelcomeQuestions';
|
||||||
@ -496,7 +497,7 @@ export function makeDocOptionsMenu(home: HomeModel, doc: Document, renaming: Obs
|
|||||||
resourceType: 'document',
|
resourceType: 'document',
|
||||||
resourceId: doc.id,
|
resourceId: doc.id,
|
||||||
resource: doc,
|
resource: doc,
|
||||||
linkToCopy: urlState().makeUrl(docUrl(doc)),
|
linkToCopy: makeShareDocUrl(doc),
|
||||||
reload: () => api.getDocAccess(doc.id),
|
reload: () => api.getDocAccess(doc.id),
|
||||||
appModel: home.app,
|
appModel: home.app,
|
||||||
});
|
});
|
||||||
|
@ -305,7 +305,7 @@ async function manageUsers(doc: DocInfo, docPageModel: DocPageModel) {
|
|||||||
resource: doc,
|
resource: doc,
|
||||||
docPageModel,
|
docPageModel,
|
||||||
appModel: docPageModel.appModel,
|
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.
|
// 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
|
// Skip if personal, since personal cannot affect "Public Access", and the only
|
||||||
// change possible is to remove the user (which would make refreshCurrentDoc fail)
|
// 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', `
|
const cssShareButton = styled('div', `
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -770,10 +770,14 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
},
|
},
|
||||||
signupFirstVisit: {
|
signupFirstVisit: {
|
||||||
category: 'ProductVisits',
|
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,
|
minimumTelemetryLevel: Level.full,
|
||||||
retentionPeriod: 'indefinitely',
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
|
loginMethod: {
|
||||||
|
description: 'The login method on getgrist.com. May be "Email + Password" or "Google".',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
siteId: {
|
siteId: {
|
||||||
description: 'The site id of first visit after signup.',
|
description: 'The site id of first visit after signup.',
|
||||||
dataType: 'number',
|
dataType: 'number',
|
||||||
@ -798,6 +802,10 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
retentionPeriod: 'indefinitely',
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
|
verificationMethod: {
|
||||||
|
description: 'The verification method. May be "code" or "link".',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
isAnonymousTemplateSignup: {
|
isAnonymousTemplateSignup: {
|
||||||
description: 'Whether the user viewed any templates before signing up.',
|
description: 'Whether the user viewed any templates before signing up.',
|
||||||
dataType: 'boolean',
|
dataType: 'boolean',
|
||||||
|
@ -144,7 +144,7 @@ describe('ApiServerAccess', function() {
|
|||||||
// We should send mail about this one, since Kiwi had no access previously.
|
// We should send mail about this one, since Kiwi had no access previously.
|
||||||
if (notificationsConfig) {
|
if (notificationsConfig) {
|
||||||
const mail = await assertLastMail();
|
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;
|
const env = mail.payload.personalizations[0].dynamic_template_data;
|
||||||
assert.deepEqual(pick(env, ['resource.name', 'resource.kind', 'resource.kindUpperFirst',
|
assert.deepEqual(pick(env, ['resource.name', 'resource.kind', 'resource.kindUpperFirst',
|
||||||
'resource.isTeamSite', 'resource.isWorkspace', 'resource.isDocument',
|
'resource.isTeamSite', 'resource.isWorkspace', 'resource.isDocument',
|
||||||
@ -159,7 +159,7 @@ describe('ApiServerAccess', function() {
|
|||||||
user: {name: 'Kiwi', email: 'kiwi@getgrist.com'},
|
user: {name: 'Kiwi', email: 'kiwi@getgrist.com'},
|
||||||
access: {role: 'editors', canEdit: true, canView: true}
|
access: {role: 'editors', canEdit: true, canView: true}
|
||||||
} as any);
|
} 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.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.from, {email: 'support@getgrist.com', name: 'Chimpy (via Grist)'});
|
||||||
assert.deepEqual(mail.payload.reply_to, {email: 'chimpy@getgrist.com', name: 'Chimpy'});
|
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);
|
const resp4 = await axios.patch(`${homeUrl}/api/orgs/${nasaOrgId}/access`, {delta: delta4}, chimpy);
|
||||||
assert.equal(resp4.status, 200);
|
assert.equal(resp4.status, 200);
|
||||||
if (notificationsConfig) {
|
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 that the number of users in the org has updated (Kiwi was added).
|
||||||
assert.deepEqual(userCountUpdates[nasaOrgId as number], [2]);
|
assert.deepEqual(userCountUpdates[nasaOrgId as number], [2]);
|
||||||
@ -349,9 +352,9 @@ describe('ApiServerAccess', function() {
|
|||||||
// Check we would sent an email to Kiwi about this
|
// Check we would sent an email to Kiwi about this
|
||||||
if (notificationsConfig) {
|
if (notificationsConfig) {
|
||||||
const mail = await assertLastMail();
|
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;
|
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.kind, 'workspace');
|
||||||
assert.equal(env.resource.kindUpperFirst, 'Workspace');
|
assert.equal(env.resource.kindUpperFirst, 'Workspace');
|
||||||
assert.equal(env.resource.isTeamSite, false);
|
assert.equal(env.resource.isTeamSite, false);
|
||||||
|
Loading…
Reference in New Issue
Block a user