(core) Polish telemetry code

Summary: Also fixes a few small bugs with telemetry collection.

Test Plan: Server and manual tests.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3915
pull/532/head
George Gevoian 12 months ago
parent 4629068c25
commit a460563daf

@ -23,764 +23,546 @@ export enum Level {
* level. * level.
*/ */
export const TelemetryContracts: TelemetryContracts = { export const TelemetryContracts: TelemetryContracts = {
/**
* Triggered when an HTTP request with an API key is made.
*/
apiUsage: { apiUsage: {
description: 'Triggered when an HTTP request with an API key is made.',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
metadataContracts: { metadataContracts: {
/**
* The HTTP request method (e.g. GET, POST, PUT).
*/
method: { method: {
description: 'The HTTP request method (e.g. GET, POST, PUT).',
dataType: 'string', dataType: 'string',
}, },
/**
* The id of the user that triggered this event.
*/
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
}, },
/**
* The User-Agent HTTP request header.
*/
userAgent: { userAgent: {
description: 'The User-Agent HTTP request header.',
dataType: 'string', dataType: 'string',
}, },
}, },
}, },
/**
* Triggered when HelpScout Beacon is opened.
*/
beaconOpen: { beaconOpen: {
description: 'Triggered when HelpScout Beacon is opened.',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
metadataContracts: { metadataContracts: {
/**
* The id of the user that triggered this event.
*/
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
}, },
/**
* A random, session-based identifier for the user that triggered this event.
*/
altSessionId: { altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string', dataType: 'string',
}, },
}, },
}, },
/**
* Triggered when an article is opened in HelpScout Beacon.
*/
beaconArticleViewed: { beaconArticleViewed: {
description: 'Triggered when an article is opened in HelpScout Beacon.',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
metadataContracts: { metadataContracts: {
/**
* The id of the article.
*/
articleId: { articleId: {
description: 'The id of the article.',
dataType: 'string', dataType: 'string',
}, },
/**
* The id of the user that triggered this event.
*/
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
}, },
/**
* A random, session-based identifier for the user that triggered this event.
*/
altSessionId: { altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string', dataType: 'string',
}, },
}, },
}, },
/**
* Triggered when an email is sent in HelpScout Beacon.
*/
beaconEmailSent: { beaconEmailSent: {
description: 'Triggered when an email is sent in HelpScout Beacon.',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
metadataContracts: { metadataContracts: {
/**
* The id of the user that triggered this event.
*/
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
}, },
/**
* A random, session-based identifier for the user that triggered this event.
*/
altSessionId: { altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string', dataType: 'string',
}, },
}, },
}, },
/**
* Triggered when a search is made in HelpScout Beacon.
*/
beaconSearch: { beaconSearch: {
description: 'Triggered when a search is made in HelpScout Beacon.',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
metadataContracts: { metadataContracts: {
/**
* The search query.
*/
searchQuery: { searchQuery: {
description: 'The search query.',
dataType: 'string', dataType: 'string',
}, },
/**
* The id of the user that triggered this event.
*/
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
}, },
/**
* A random, session-based identifier for the user that triggered this event.
*/
altSessionId: { altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string', dataType: 'string',
}, },
}, },
}, },
/**
* Triggered when a document is forked.
*/
documentForked: { documentForked: {
description: 'Triggered when a document is forked.',
minimumTelemetryLevel: Level.limited, minimumTelemetryLevel: Level.limited,
metadataContracts: { metadataContracts: {
/**
* A hash of the doc id.
*/
docIdDigest: { docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string', dataType: 'string',
}, },
/**
* The id of the site containing the forked document.
*/
siteId: { siteId: {
description: 'The id of the site containing the forked document.',
dataType: 'number', dataType: 'number',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* The type of the site.
*/
siteType: { siteType: {
description: 'The type of the site.',
dataType: 'string', dataType: 'string',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* A random, session-based identifier for the user that triggered this event.
*/
altSessionId: { altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string', dataType: 'string',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/** access: {
* The id of the user that triggered this event. description: 'The document access level of the user that triggered this event.',
*/ dataType: 'string',
},
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* A hash of the fork id.
*/
forkIdDigest: { forkIdDigest: {
description: 'A hash of the fork id.',
dataType: 'string', dataType: 'string',
}, },
/**
* A hash of the full id of the fork, including the trunk id and fork id.
*/
forkDocIdDigest: { forkDocIdDigest: {
description: 'A hash of the full id of the fork, including the trunk id and fork id.',
dataType: 'string', dataType: 'string',
}, },
/**
* A hash of the trunk id.
*/
trunkIdDigest: { trunkIdDigest: {
description: 'A hash of the trunk id.',
dataType: 'string', dataType: 'string',
}, },
/**
* Whether the trunk is a template.
*/
isTemplate: { isTemplate: {
description: 'Whether the trunk is a template.',
dataType: 'boolean', dataType: 'boolean',
}, },
/**
* Timestamp of the last update to the trunk document.
*/
lastActivity: { lastActivity: {
description: 'Timestamp of the last update to the trunk document.',
dataType: 'date', dataType: 'date',
}, },
}, },
}, },
/**
* Triggered when a public document or template is opened.
*/
documentOpened: { documentOpened: {
description: 'Triggered when a public document or template is opened.',
minimumTelemetryLevel: Level.limited, minimumTelemetryLevel: Level.limited,
metadataContracts: { metadataContracts: {
/**
* A hash of the doc id.
*/
docIdDigest: { docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string', dataType: 'string',
}, },
/**
* The site id.
*/
siteId: { siteId: {
description: 'The site id.',
dataType: 'number', dataType: 'number',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* The site type.
*/
siteType: { siteType: {
description: 'The site type.',
dataType: 'string', dataType: 'string',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* The id of the user that triggered this event.
*/
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* A random, session-based identifier for the user that triggered this event.
*/
altSessionId: { altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string', dataType: 'string',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* The document access level of the user that triggered this event.
*/
access: { access: {
dataType: 'boolean', description: 'The document access level of the user that triggered this event.',
dataType: 'string',
}, },
/**
* Whether the document is public.
*/
isPublic: { isPublic: {
description: 'Whether the document is public.',
dataType: 'boolean', dataType: 'boolean',
}, },
/**
* Whether a snapshot was opened.
*/
isSnapshot: { isSnapshot: {
description: 'Whether a snapshot was opened.',
dataType: 'boolean', dataType: 'boolean',
}, },
/**
* Whether the document is a template.
*/
isTemplate: { isTemplate: {
description: 'Whether the document is a template.',
dataType: 'boolean', dataType: 'boolean',
}, },
/**
* Timestamp of when the document was last updated.
*/
lastUpdated: { lastUpdated: {
description: 'Timestamp of when the document was last updated.',
dataType: 'date', dataType: 'date',
}, },
}, },
}, },
/**
* Triggered on doc open and close, as well as hourly while a document is open.
*/
documentUsage: { documentUsage: {
description: 'Triggered on doc open and close, as well as hourly while a document is open.',
minimumTelemetryLevel: Level.limited, minimumTelemetryLevel: Level.limited,
metadataContracts: { metadataContracts: {
/**
* A hash of the doc id.
*/
docIdDigest: { docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string', dataType: 'string',
}, },
/**
* The site id.
*/
siteId: { siteId: {
description: 'The site id.',
dataType: 'number', dataType: 'number',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* The site type.
*/
siteType: { siteType: {
description: 'The site type.',
dataType: 'string', dataType: 'string',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* A random, session-based identifier for the user that triggered this event.
*/
altSessionId: { altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string', dataType: 'string',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/** access: {
* The id of the user that triggered this event. description: 'The document access level of the user that triggered this event.',
*/ dataType: 'string',
},
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* What caused this event to trigger.
*
* May be either "docOpen", "interval", or "docClose".
*/
triggeredBy: { triggeredBy: {
description: 'What caused this event to trigger. May be either "docOpen", "interval", or "docClose".',
dataType: 'string', dataType: 'string',
}, },
/**
* Whether the document is public.
*/
isPublic: { isPublic: {
description: 'Whether the document is public.',
dataType: 'boolean', dataType: 'boolean',
}, },
/**
* The number of rows in the document.
*/
rowCount: { rowCount: {
description: 'The number of rows in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The total size of all data in the document, excluding attachments.
*/
dataSizeBytes: { dataSizeBytes: {
description: 'The total size of all data in the document, excluding attachments.',
dataType: 'number', dataType: 'number',
}, },
/**
* The total size of all attachments in the document.
*/
attachmentsSize: { attachmentsSize: {
description: 'The total size of all attachments in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of access rules in the document.
*/
numAccessRules: { numAccessRules: {
description: 'The number of access rules in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of user attributes in the document.
*/
numUserAttributes: { numUserAttributes: {
description: 'The number of user attributes in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of attachments in the document.
*/
numAttachments: { numAttachments: {
description: 'The number of attachments in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* A list of unique file extensions compiled from all of the document's attachments.
*/
attachmentTypes: { attachmentTypes: {
description: "A list of unique file extensions compiled from all of the document's attachments.",
dataType: 'string[]', dataType: 'string[]',
}, },
/**
* The number of charts in the document.
*/
numCharts: { numCharts: {
description: 'The number of charts in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* A list of chart types of every chart in the document.
*/
chartTypes: { chartTypes: {
description: 'A list of chart types of every chart in the document.',
dataType: 'string[]', dataType: 'string[]',
}, },
/**
* The number of linked charts in the document.
*/
numLinkedCharts: { numLinkedCharts: {
description: 'The number of linked charts in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of linked widgets in the document.
*/
numLinkedWidgets: { numLinkedWidgets: {
description: 'The number of linked widgets in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of columns in the document.
*/
numColumns: { numColumns: {
description: 'The number of columns in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of columns with conditional formatting in the document.
*/
numColumnsWithConditionalFormatting: { numColumnsWithConditionalFormatting: {
description: 'The number of columns with conditional formatting in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of formula columns in the document.
*/
numFormulaColumns: { numFormulaColumns: {
description: 'The number of formula columns in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of trigger formula columns in the document.
*/
numTriggerFormulaColumns: { numTriggerFormulaColumns: {
description: 'The number of trigger formula columns in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of summary formula columns in the document.
*/
numSummaryFormulaColumns: { numSummaryFormulaColumns: {
description: 'The number of summary formula columns in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of fields with conditional formatting in the document.
*/
numFieldsWithConditionalFormatting: { numFieldsWithConditionalFormatting: {
description: 'The number of fields with conditional formatting in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of tables in the document.
*/
numTables: { numTables: {
description: 'The number of tables in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of on-demand tables in the document.
*/
numOnDemandTables: { numOnDemandTables: {
description: 'The number of on-demand tables in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of tables with conditional formatting in the document.
*/
numTablesWithConditionalFormatting: { numTablesWithConditionalFormatting: {
description: 'The number of tables with conditional formatting in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of summary tables in the document.
*/
numSummaryTables: { numSummaryTables: {
description: 'The number of summary tables in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of custom widgets in the document.
*/
numCustomWidgets: { numCustomWidgets: {
description: 'The number of custom widgets in the document.',
dataType: 'number', dataType: 'number',
}, },
/**
* A list of plugin ids for every custom widget in the document.
*
* The ids of widgets not created by Grist Labs are replaced with "externalId".
*/
customWidgetIds: { customWidgetIds: {
description: 'A list of plugin ids for every custom widget in the document. '
+ 'The ids of widgets not created by Grist Labs are replaced with "externalId".',
dataType: 'string[]', dataType: 'string[]',
}, },
}, },
}, },
/**
* Triggered every 5 seconds.
*/
processMonitor: { processMonitor: {
description: 'Triggered every 5 seconds.',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
metadataContracts: { metadataContracts: {
/** Size of JS heap in use, in MiB. */
heapUsedMB: { heapUsedMB: {
description: 'Size of JS heap in use, in MiB.',
dataType: 'number', dataType: 'number',
}, },
/** Total heap size, in MiB, allocated for JS by V8. */
heapTotalMB: { heapTotalMB: {
description: 'Total heap size, in MiB, allocated for JS by V8. ',
dataType: 'number', dataType: 'number',
}, },
/** Fraction (typically between 0 and 1) of CPU usage. Includes all threads, so may exceed 1. */
cpuAverage: { cpuAverage: {
description: 'Fraction (typically between 0 and 1) of CPU usage. Includes all threads, so may exceed 1.',
dataType: 'number', dataType: 'number',
}, },
/** Interval (in milliseconds) over which `cpuAverage` is reported. */
intervalMs: { intervalMs: {
description: 'Interval (in milliseconds) over which `cpuAverage` is reported.',
dataType: 'number', dataType: 'number',
}, },
}, },
}, },
/**
* Triggered when sending webhooks.
*/
sendingWebhooks: { sendingWebhooks: {
description: 'Triggered when sending webhooks.',
minimumTelemetryLevel: Level.limited, minimumTelemetryLevel: Level.limited,
metadataContracts: { metadataContracts: {
/**
* The number of events in the batch of webhooks being sent.
*/
numEvents: { numEvents: {
description: 'The number of events in the batch of webhooks being sent.',
dataType: 'number', dataType: 'number',
}, },
/**
* A hash of the doc id.
*/
docIdDigest: { docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string', dataType: 'string',
}, },
/**
* The site id.
*/
siteId: { siteId: {
description: 'The site id.',
dataType: 'number', dataType: 'number',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* The site type.
*/
siteType: { siteType: {
description: 'The site type.',
dataType: 'string', dataType: 'string',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* A random, session-based identifier for the user that triggered this event.
*/
altSessionId: { altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string', dataType: 'string',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/** access: {
* The id of the user that triggered this event. description: 'The document access level of the user that triggered this event.',
*/ dataType: 'string',
},
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
}, },
}, },
/**
* Triggered after a user successfully verifies their account during sign-up.
*
* Not triggered in grist-core.
*/
signupVerified: { signupVerified: {
description: 'Triggered after a user successfully verifies their account during sign-up. '
+ 'Not triggered in grist-core.',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
metadataContracts: { metadataContracts: {
/**
* Whether the user viewed any templates before signing up.
*/
isAnonymousTemplateSignup: { isAnonymousTemplateSignup: {
description: 'Whether the user viewed any templates before signing up.',
dataType: 'boolean', dataType: 'boolean',
}, },
/**
* The doc id of the template the user last viewed before signing up, if any.
*/
templateId: { templateId: {
description: 'The doc id of the template the user last viewed before signing up, if any.',
dataType: 'string', dataType: 'string',
}, },
}, },
}, },
/**
* Triggered daily.
*/
siteMembership: { siteMembership: {
description: 'Triggered daily.',
minimumTelemetryLevel: Level.limited, minimumTelemetryLevel: Level.limited,
metadataContracts: { metadataContracts: {
/**
* The site id.
*/
siteId: { siteId: {
description: 'The site id.',
dataType: 'number', dataType: 'number',
}, },
/**
* The site type.
*/
siteType: { siteType: {
description: 'The site type.',
dataType: 'string', dataType: 'string',
}, },
/**
* The number of users with an owner role in this site.
*/
numOwners: { numOwners: {
description: 'The number of users with an owner role in this site.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of users with an editor role in this site.
*/
numEditors: { numEditors: {
description: 'The number of users with an editor role in this site.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of users with a viewer role in this site.
*/
numViewers: { numViewers: {
description: 'The number of users with a viewer role in this site.',
dataType: 'number', dataType: 'number',
}, },
}, },
}, },
/**
* Triggered daily.
*/
siteUsage: { siteUsage: {
description: 'Triggered daily.',
minimumTelemetryLevel: Level.limited, minimumTelemetryLevel: Level.limited,
metadataContracts: { metadataContracts: {
/**
* The site id.
*/
siteId: { siteId: {
description: 'The site id.',
dataType: 'number', dataType: 'number',
}, },
/**
* The site type.
*/
siteType: { siteType: {
description: 'The site type.',
dataType: 'string', dataType: 'string',
}, },
/**
* Whether the site's subscription is in good standing.
*/
inGoodStanding: { inGoodStanding: {
description: "Whether the site's subscription is in good standing.",
dataType: 'boolean', dataType: 'boolean',
}, },
/**
* The Stripe Plan id associated with this site.
*/
stripePlanId: { stripePlanId: {
description: 'The Stripe Plan id associated with this site.',
dataType: 'string', dataType: 'string',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* The number of docs in this site.
*/
numDocs: { numDocs: {
description: 'The number of docs in this site.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of workspaces in this site.
*/
numWorkspaces: { numWorkspaces: {
description: 'The number of workspaces in this site.',
dataType: 'number', dataType: 'number',
}, },
/**
* The number of site members.
*/
numMembers: { numMembers: {
description: 'The number of site members.',
dataType: 'number', dataType: 'number',
}, },
/**
* A timestamp of the most recent update made to a site document.
*/
lastActivity: { lastActivity: {
description: 'A timestamp of the most recent update made to a site document.',
dataType: 'date', dataType: 'date',
}, },
}, },
}, },
/**
* Triggered on changes to tutorial progress.
*/
tutorialProgressChanged: { tutorialProgressChanged: {
description: 'Triggered on changes to tutorial progress.',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
metadataContracts: { metadataContracts: {
/**
* A hash of the tutorial fork id.
*/
tutorialForkIdDigest: { tutorialForkIdDigest: {
description: 'A hash of the tutorial fork id.',
dataType: 'string', dataType: 'string',
}, },
/**
* A hash of the tutorial trunk id.
*/
tutorialTrunkIdDigest: { tutorialTrunkIdDigest: {
description: 'A hash of the tutorial trunk id.',
dataType: 'string', dataType: 'string',
}, },
/**
* The 0-based index of the last tutorial slide the user had open.
*/
lastSlideIndex: { lastSlideIndex: {
description: 'The 0-based index of the last tutorial slide the user had open.',
dataType: 'number', dataType: 'number',
}, },
/**
* The total number of slides in the tutorial.
*/
numSlides: { numSlides: {
description: 'The total number of slides in the tutorial.',
dataType: 'number', dataType: 'number',
}, },
/**
* Percentage of tutorial completion.
*/
percentComplete: { percentComplete: {
description: 'Percentage of tutorial completion.',
dataType: 'number', dataType: 'number',
}, },
}, },
}, },
/**
* Triggered when a tutorial is restarted.
*/
tutorialRestarted: { tutorialRestarted: {
description: 'Triggered when a tutorial is restarted.',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
metadataContracts: { metadataContracts: {
/**
* A hash of the tutorial fork id.
*/
tutorialForkIdDigest: { tutorialForkIdDigest: {
description: 'A hash of the tutorial fork id.',
dataType: 'string', dataType: 'string',
}, },
/**
* A hash of the tutorial trunk id.
*/
tutorialTrunkIdDigest: { tutorialTrunkIdDigest: {
description: 'A hash of the tutorial trunk id.',
dataType: 'string', dataType: 'string',
}, },
/**
* A hash of the doc id.
*/
docIdDigest: { docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string', dataType: 'string',
}, },
/**
* The site id.
*/
siteId: { siteId: {
description: 'The site id.',
dataType: 'number', dataType: 'number',
}, },
/**
* The site type.
*/
siteType: { siteType: {
description: 'The site type.',
dataType: 'string', dataType: 'string',
}, },
/**
* A random, session-based identifier for the user that triggered this event.
*/
altSessionId: { altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
access: {
description: 'The document access level of the user that triggered this event.',
dataType: 'string', dataType: 'string',
}, },
/**
* The id of the user that triggered this event.
*/
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
}, },
}, },
}, },
/**
* Triggered when the video tour is closed.
*/
watchedVideoTour: { watchedVideoTour: {
description: 'Triggered when the video tour is closed.',
minimumTelemetryLevel: Level.limited, minimumTelemetryLevel: Level.limited,
metadataContracts: { metadataContracts: {
/**
* The number of seconds elapsed in the video player.
*/
watchTimeSeconds: { watchTimeSeconds: {
description: 'The number of seconds elapsed in the video player.',
dataType: 'number', dataType: 'number',
}, },
/**
* The id of the user that triggered this event.
*/
userId: { userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number', dataType: 'number',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
/**
* A random, session-based identifier for the user that triggered this event.
*/
altSessionId: { altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string', dataType: 'string',
minimumTelemetryLevel: Level.full, minimumTelemetryLevel: Level.full,
}, },
@ -811,11 +593,13 @@ export const TelemetryEvents = StringUnion(
export type TelemetryEvent = typeof TelemetryEvents.type; export type TelemetryEvent = typeof TelemetryEvents.type;
interface TelemetryEventContract { interface TelemetryEventContract {
description: string;
minimumTelemetryLevel: Level; minimumTelemetryLevel: Level;
metadataContracts?: Record<string, MetadataContract>; metadataContracts?: Record<string, MetadataContract>;
} }
interface MetadataContract { interface MetadataContract {
description: string;
dataType: 'boolean' | 'number' | 'string' | 'string[]' | 'date'; dataType: 'boolean' | 'number' | 'string' | 'string[]' | 'date';
minimumTelemetryLevel?: Level; minimumTelemetryLevel?: Level;
} }

@ -130,12 +130,12 @@ export class Document extends Resource {
this.options.tutorial = null; this.options.tutorial = null;
} else { } else {
this.options.tutorial = this.options.tutorial || {}; this.options.tutorial = this.options.tutorial || {};
if (props.options.tutorial.lastSlideIndex !== undefined) {
this.options.tutorial.lastSlideIndex = props.options.tutorial.lastSlideIndex;
}
if (props.options.tutorial.numSlides !== undefined) { if (props.options.tutorial.numSlides !== undefined) {
this.options.tutorial.numSlides = props.options.tutorial.numSlides; this.options.tutorial.numSlides = props.options.tutorial.numSlides;
if (dbManager && props.options?.tutorial?.lastSlideIndex !== undefined) { }
if (props.options.tutorial.lastSlideIndex !== undefined) {
this.options.tutorial.lastSlideIndex = props.options.tutorial.lastSlideIndex;
if (dbManager && this.options.tutorial.numSlides) {
this._emitTutorialProgressChangeEvent(dbManager, this.options.tutorial); this._emitTutorialProgressChangeEvent(dbManager, this.options.tutorial);
} }
} }

@ -906,6 +906,7 @@ export class DocWorkerApi {
name: tutorialTrunk.name, name: tutorialTrunk.name,
options: { options: {
tutorial: { tutorial: {
...tutorialTrunk.options?.tutorial,
// For now, the only state we need to reset is the slide position. // For now, the only state we need to reset is the slide position.
lastSlideIndex: 0, lastSlideIndex: 0,
}, },

@ -25,6 +25,8 @@ export interface ITelemetry {
getTelemetryLevel(): TelemetryLevel; getTelemetryLevel(): TelemetryLevel;
} }
const MAX_PENDING_FORWARD_EVENT_REQUESTS = 25;
/** /**
* Manages telemetry for Grist. * Manages telemetry for Grist.
*/ */
@ -34,10 +36,11 @@ export class Telemetry implements ITelemetry {
private _shouldForwardTelemetryEvents = this._deploymentType !== 'saas'; private _shouldForwardTelemetryEvents = this._deploymentType !== 'saas';
private _forwardTelemetryEventsUrl = process.env.GRIST_TELEMETRY_URL || private _forwardTelemetryEventsUrl = process.env.GRIST_TELEMETRY_URL ||
'https://telemetry.getgrist.com/api/telemetry'; 'https://telemetry.getgrist.com/api/telemetry';
private _numPendingForwardEventRequests = 0;
private _installationId: string | undefined; private _installationId: string | undefined;
private _errorLogger = new LogMethods('Telemetry ', () => ({})); private _logger = new LogMethods('Telemetry ', () => ({}));
private _telemetryLogger = new LogMethods('Telemetry ', () => ({ private _telemetryLogger = new LogMethods('Telemetry ', () => ({
eventType: 'telemetry', eventType: 'telemetry',
})); }));
@ -46,7 +49,7 @@ export class Telemetry implements ITelemetry {
constructor(private _dbManager: HomeDBManager, private _gristServer: GristServer) { constructor(private _dbManager: HomeDBManager, private _gristServer: GristServer) {
this._initialize().catch((e) => { this._initialize().catch((e) => {
this._errorLogger.error(undefined, 'failed to initialize', e); this._logger.error(undefined, 'failed to initialize', e);
}); });
} }
@ -99,7 +102,7 @@ export class Telemetry implements ITelemetry {
this._checkTelemetryEvent(event, metadata); this._checkTelemetryEvent(event, metadata);
if (this._shouldForwardTelemetryEvents) { if (this._shouldForwardTelemetryEvents) {
await this.forwardEvent(event, metadata); await this._forwardEvent(event, metadata);
} else { } else {
this._telemetryLogger.rawLog('info', null, event, { this._telemetryLogger.rawLog('info', null, event, {
eventName: event, eventName: event,
@ -109,29 +112,6 @@ export class Telemetry implements ITelemetry {
} }
} }
/**
* Forwards a telemetry event and its metadata to another server.
*/
public async forwardEvent(
event: TelemetryEvent,
metadata?: TelemetryMetadata
) {
try {
await fetch(this._forwardTelemetryEventsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
event,
metadata,
}),
});
} catch (e) {
this._errorLogger.error(undefined, `failed to forward telemetry event ${event}`, e);
}
}
public addEndpoints(app: express.Application) { public addEndpoints(app: express.Application) {
/** /**
* Logs telemetry events and their metadata. * Logs telemetry events and their metadata.
@ -169,7 +149,7 @@ export class Telemetry implements ITelemetry {
req.body.metadata, req.body.metadata,
)); ));
} catch (e) { } catch (e) {
this._errorLogger.error(undefined, `failed to log telemetry event ${event}`, e); this._logger.error(undefined, `failed to log telemetry event ${event}`, e);
throw new ApiError(`Telemetry failed to log telemetry event ${event}`, 500); throw new ApiError(`Telemetry failed to log telemetry event ${event}`, 500);
} }
} }
@ -195,7 +175,7 @@ export class Telemetry implements ITelemetry {
for (const event of HomeDBTelemetryEvents.values) { for (const event of HomeDBTelemetryEvents.values) {
this._dbManager.on(event, async (metadata) => { this._dbManager.on(event, async (metadata) => {
this.logEvent(event, metadata).catch(e => this.logEvent(event, metadata).catch(e =>
this._errorLogger.error(undefined, `failed to log telemetry event ${event}`, e)); this._logger.error(undefined, `failed to log telemetry event ${event}`, e));
}); });
} }
} }
@ -207,4 +187,34 @@ export class Telemetry implements ITelemetry {
this._checkEvent(event, metadata); this._checkEvent(event, metadata);
} }
private async _forwardEvent(
event: TelemetryEvent,
metadata?: TelemetryMetadata
) {
if (this._numPendingForwardEventRequests === MAX_PENDING_FORWARD_EVENT_REQUESTS) {
this._logger.warn(undefined, 'exceeded the maximum number of pending forwardEvent calls '
+ `(${MAX_PENDING_FORWARD_EVENT_REQUESTS}). Skipping forwarding of event ${event}.`);
return;
}
try {
this._numPendingForwardEventRequests += 1;
await this._postJsonPayload(JSON.stringify({event, metadata}));
} catch (e) {
this._logger.error(undefined, `failed to forward telemetry event ${event}`, e);
} finally {
this._numPendingForwardEventRequests -= 1;
}
}
private async _postJsonPayload(payload: string) {
await fetch(this._forwardTelemetryEventsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: payload,
});
}
} }

@ -1,5 +1,5 @@
import {GristDeploymentType} from 'app/common/gristUrls'; import {GristDeploymentType} from 'app/common/gristUrls';
import {TelemetryEvent, TelemetryLevel, TelemetryMetadata} from 'app/common/Telemetry'; import {TelemetryEvent, TelemetryLevel} from 'app/common/Telemetry';
import {ILogMeta, LogMethods} from 'app/server/lib/LogMethods'; import {ILogMeta, LogMethods} from 'app/server/lib/LogMethods';
import {ITelemetry, Telemetry} from 'app/server/lib/Telemetry'; import {ITelemetry, Telemetry} from 'app/server/lib/Telemetry';
import axios from 'axios'; import axios from 'axios';
@ -27,10 +27,11 @@ describe('Telemetry', function() {
let installationId: string; let installationId: string;
let server: TestServer; let server: TestServer;
let telemetry: ITelemetry; let telemetry: ITelemetry;
let forwardEventSpy: sinon.SinonSpy;
let postJsonPayloadStub: sinon.SinonStub;
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
const loggedEvents: [TelemetryEvent, ILogMeta][] = []; const loggedEvents: [TelemetryEvent, ILogMeta][] = [];
const forwardedEvents: [TelemetryEvent, TelemetryMetadata | undefined][] = [];
before(async function() { before(async function() {
process.env.GRIST_TEST_SERVER_DEPLOYMENT_TYPE = deploymentType; process.env.GRIST_TEST_SERVER_DEPLOYMENT_TYPE = deploymentType;
@ -43,11 +44,10 @@ describe('Telemetry', function() {
.callsFake((_level: string, _info: unknown, name: string, meta: ILogMeta) => { .callsFake((_level: string, _info: unknown, name: string, meta: ILogMeta) => {
loggedEvents.push([name as TelemetryEvent, meta]); loggedEvents.push([name as TelemetryEvent, meta]);
}); });
sandbox forwardEventSpy = sandbox
.stub(Telemetry.prototype, 'forwardEvent') .spy(Telemetry.prototype as any, '_forwardEvent');
.callsFake((event: TelemetryEvent, metadata?: TelemetryMetadata) => { postJsonPayloadStub = sandbox
forwardedEvents.push([event, metadata]); .stub(Telemetry.prototype as any, '_postJsonPayload');
});
telemetry = server.server.getTelemetry(); telemetry = server.server.getTelemetry();
}); });
@ -106,7 +106,7 @@ describe('Telemetry', function() {
} }
assert.equal(loggedEvents.length, 1); assert.equal(loggedEvents.length, 1);
assert.isEmpty(forwardedEvents); assert.equal(forwardEventSpy.callCount, 0);
}); });
} else { } else {
it('forwards telemetry events', async function() { it('forwards telemetry events', async function() {
@ -117,7 +117,7 @@ describe('Telemetry', function() {
isPublic: false, isPublic: false,
}, },
}); });
assert.deepEqual(forwardedEvents[forwardedEvents.length - 1], [ assert.deepEqual(forwardEventSpy.lastCall.args, [
'documentOpened', 'documentOpened',
{ {
docIdDigest: 'digest', docIdDigest: 'digest',
@ -136,7 +136,7 @@ describe('Telemetry', function() {
userId: 1, userId: 1,
}, },
}); });
assert.deepEqual(forwardedEvents[forwardedEvents.length - 1], [ assert.deepEqual(forwardEventSpy.lastCall.args, [
'documentOpened', 'documentOpened',
{ {
docIdDigest: 'digest', docIdDigest: 'digest',
@ -146,7 +146,7 @@ describe('Telemetry', function() {
]); ]);
} }
assert.equal(forwardedEvents.length, 1); assert.equal(forwardEventSpy.callCount, 1);
assert.isEmpty(loggedEvents); assert.isEmpty(loggedEvents);
}); });
} }
@ -159,7 +159,7 @@ describe('Telemetry', function() {
}, },
}); });
assert.isEmpty(loggedEvents); assert.isEmpty(loggedEvents);
assert.isEmpty(forwardedEvents); assert.equal(forwardEventSpy.callCount, 0);
}); });
} }
@ -231,7 +231,7 @@ describe('Telemetry', function() {
assert.equal(loggedEvents.length, 3); assert.equal(loggedEvents.length, 3);
assert.equal(loggedEvents[1][0], 'apiUsage'); assert.equal(loggedEvents[1][0], 'apiUsage');
} }
assert.isEmpty(forwardedEvents); assert.equal(forwardEventSpy.callCount, 0);
}); });
if (telemetryLevel === 'limited') { if (telemetryLevel === 'limited') {
@ -256,7 +256,7 @@ describe('Telemetry', function() {
assert.equal(metadata.watchTimeSeconds, 60); assert.equal(metadata.watchTimeSeconds, 60);
assert.equal(metadata.userId, 123); assert.equal(metadata.userId, 123);
assert.equal(loggedEvents.length, 3); assert.equal(loggedEvents.length, 3);
assert.isEmpty(forwardedEvents); assert.equal(forwardEventSpy.callCount, 0);
}); });
} }
} else { } else {
@ -267,7 +267,7 @@ describe('Telemetry', function() {
limited: {watchTimeSeconds: 30}, limited: {watchTimeSeconds: 30},
}, },
}, chimpy); }, chimpy);
const [event, metadata] = forwardedEvents[forwardedEvents.length - 1]; const [event, metadata] = forwardEventSpy.lastCall.args;
assert.equal(event, 'watchedVideoTour'); assert.equal(event, 'watchedVideoTour');
if (telemetryLevel === 'limited') { if (telemetryLevel === 'limited') {
assert.deepEqual(metadata, { assert.deepEqual(metadata, {
@ -283,25 +283,48 @@ describe('Telemetry', function() {
'userId', 'userId',
'altSessionId', 'altSessionId',
]); ]);
assert.equal(metadata!.watchTimeSeconds, 30); assert.equal(metadata.watchTimeSeconds, 30);
assert.equal(metadata!.userId, 1); assert.equal(metadata.userId, 1);
} }
if (telemetryLevel === 'limited') { if (telemetryLevel === 'limited') {
assert.equal(forwardedEvents.length, 2); assert.equal(forwardEventSpy.callCount, 2);
} else { } else {
// The POST above also triggers an "apiUsage" event. // The POST above also triggers an "apiUsage" event.
assert.equal(forwardedEvents.length, 3); assert.equal(forwardEventSpy.callCount, 3);
assert.equal(forwardedEvents[1][0], 'apiUsage'); assert.equal(forwardEventSpy.secondCall.args[0], 'apiUsage');
} }
assert.isEmpty(loggedEvents); assert.isEmpty(loggedEvents);
}); });
it('skips forwarding events if too many requests are pending', async function() {
let numRequestsMade = 0;
postJsonPayloadStub.callsFake(async () => {
numRequestsMade += 1;
await new Promise(resolve => setTimeout(resolve, 1000));
});
forwardEventSpy.resetHistory();
// Log enough events simultaneously to cause some to be skipped. (The limit is 25.)
for (let i = 0; i < 30; i++) {
void telemetry.logEvent('documentOpened', {
limited: {
docIdDigest: 'digest',
isPublic: false,
},
});
}
// Check that out of the 30 forwardEvent calls, only 25 made POST requests.
assert.equal(forwardEventSpy.callCount, 30);
assert.equal(numRequestsMade, 25);
});
} }
} else { } else {
it('does not log telemetry events sent to /api/telemetry', async function() { it('does not log telemetry events sent to /api/telemetry', async function() {
await telemetry.logEvent('apiUsage', {limited: {method: 'GET'}}); await telemetry.logEvent('apiUsage', {limited: {method: 'GET'}});
assert.isEmpty(loggedEvents); assert.isEmpty(loggedEvents);
assert.isEmpty(forwardedEvents); assert.equal(forwardEventSpy.callCount, 0);
}); });
} }
}); });

Loading…
Cancel
Save