gristlabs_grist-core/app/common/Telemetry.ts
Paul Fitzpatrick bc6f550471
normalize logging of dates in siteUsage across databases (#675)
Date handling for SQLite and PostgreSQL is inconsistent. This makes
sure that dates in `siteUsage` logs are in a consistent format that
includes time zone information.
2023-09-13 14:44:04 -04:00

1012 lines
32 KiB
TypeScript

import {StringUnion} from 'app/common/StringUnion';
/**
* Telemetry levels, in increasing order of data collected.
*/
export enum Level {
off = 0,
limited = 1,
full = 2,
}
/**
* A set of contracts that all telemetry events must follow prior to being
* logged.
*
* Currently, this includes meeting minimum telemetry levels for events
* and their metadata, and passing in the correct data type for the value of
* each metadata property.
*
* The `minimumTelemetryLevel` defined at the event level will also be applied
* to all metadata properties of an event, and can be overridden at the metadata
* level.
*/
export const TelemetryContracts: TelemetryContracts = {
apiUsage: {
description: 'Triggered when an HTTP request with an API key is made.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
method: {
description: 'The HTTP request method (e.g. GET, POST, PUT).',
dataType: 'string',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
userAgent: {
description: 'The User-Agent HTTP request header.',
dataType: 'string',
},
},
},
assistantOpen: {
description: 'Triggered when the AI Assistant is first opened.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'short',
metadataContracts: {
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
conversationId: {
description: 'A random identifier for the current conversation with the assistant.',
dataType: 'string',
},
context: {
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
dataType: 'object',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
assistantSend: {
description: 'Triggered when a message is sent to the AI Assistant.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'short',
metadataContracts: {
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
siteId: {
description: 'The id of the site.',
dataType: 'number',
},
siteType: {
description: 'The type of the site.',
dataType: 'string',
},
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',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
conversationId: {
description: 'A random identifier for the current conversation with the assistant.',
dataType: 'string',
},
context: {
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
dataType: 'object',
},
prompt: {
description: 'The role ("user" or "system"), content, and index of the message sent to the AI Assistant.',
dataType: 'object',
},
},
},
assistantReceive: {
description: 'Triggered when a message is received from the AI Assistant is received.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'short',
metadataContracts: {
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
siteId: {
description: 'The id of the site.',
dataType: 'number',
},
siteType: {
description: 'The type of the site.',
dataType: 'string',
},
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',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
conversationId: {
description: 'A random identifier for the current conversation with the assistant.',
dataType: 'string',
},
context: {
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
dataType: 'object',
},
message: {
description: 'The content and index of the message received from the AI Assistant.',
dataType: 'object',
},
suggestedFormula: {
description: 'The formula suggested by the AI Assistant, if present.',
dataType: 'string',
},
},
},
assistantSave: {
description: 'Triggered when changes in the expanded formula editor are saved after the AI Assistant ' +
'was opened.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'short',
metadataContracts: {
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
conversationId: {
description: 'A random identifier for the current conversation with the assistant.',
dataType: 'string',
},
context: {
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
dataType: 'object',
},
newFormula: {
description: 'The formula that was saved.',
dataType: 'string',
},
oldFormula: {
description: 'The formula that was overwritten.',
dataType: 'string',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
assistantCancel: {
description: 'Triggered when changes in the expanded formula editor are discarded after the AI Assistant ' +
'was opened.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'short',
metadataContracts: {
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
conversationId: {
description: 'A random identifier for the current conversation with the assistant.',
dataType: 'string',
},
context: {
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
dataType: 'object',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
assistantClearConversation: {
description: 'Triggered when a conversation in the AI Assistant is cleared.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'short',
metadataContracts: {
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
conversationId: {
description: 'A random identifier for the current conversation with the assistant.',
dataType: 'string',
},
context: {
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
dataType: 'object',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
assistantClose: {
description: 'Triggered when a formula is saved or discarded after the AI Assistant was opened.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
conversationId: {
description: 'A random identifier for the current conversation with the assistant.',
dataType: 'string',
},
suggestionApplied: {
description: 'True if a suggested formula from one of the received messages was applied.',
dataType: 'boolean',
},
conversationLength: {
description: 'The number of messages sent and received since opening the AI Assistant.',
dataType: 'number',
},
conversationHistoryLength: {
description: "The number of messages in the conversation's history. May be less than conversationLength "
+ "if the conversation history was cleared in the same session.",
dataType: 'number',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
beaconOpen: {
description: 'Triggered when HelpScout Beacon is opened.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
beaconArticleViewed: {
description: 'Triggered when an article is opened in HelpScout Beacon.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
articleId: {
description: 'The id of the article.',
dataType: 'string',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
beaconEmailSent: {
description: 'Triggered when an email is sent in HelpScout Beacon.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
beaconSearch: {
description: 'Triggered when a search is made in HelpScout Beacon.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
searchQuery: {
description: 'The search query.',
dataType: 'string',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
documentForked: {
description: 'Triggered when a document is forked.',
minimumTelemetryLevel: Level.limited,
retentionPeriod: 'indefinitely',
metadataContracts: {
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
siteId: {
description: 'The id of the site containing the forked document.',
dataType: 'number',
minimumTelemetryLevel: Level.full,
},
siteType: {
description: 'The type of the site.',
dataType: 'string',
minimumTelemetryLevel: Level.full,
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
minimumTelemetryLevel: Level.full,
},
access: {
description: 'The document access level of the user that triggered this event.',
dataType: 'string',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
minimumTelemetryLevel: Level.full,
},
forkIdDigest: {
description: 'A hash of the fork id.',
dataType: 'string',
},
forkDocIdDigest: {
description: 'A hash of the full id of the fork, including the trunk id and fork id.',
dataType: 'string',
},
trunkIdDigest: {
description: 'A hash of the trunk id.',
dataType: 'string',
},
isTemplate: {
description: 'Whether the trunk is a template.',
dataType: 'boolean',
},
lastActivity: {
description: 'Timestamp of the last update to the trunk document.',
dataType: 'date',
},
},
},
documentOpened: {
description: 'Triggered when a public document or template is opened.',
minimumTelemetryLevel: Level.limited,
retentionPeriod: 'indefinitely',
metadataContracts: {
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
siteId: {
description: 'The site id.',
dataType: 'number',
minimumTelemetryLevel: Level.full,
},
siteType: {
description: 'The site type.',
dataType: 'string',
minimumTelemetryLevel: Level.full,
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
minimumTelemetryLevel: Level.full,
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
minimumTelemetryLevel: Level.full,
},
access: {
description: 'The document access level of the user that triggered this event.',
dataType: 'string',
},
isPublic: {
description: 'Whether the document is public.',
dataType: 'boolean',
},
isSnapshot: {
description: 'Whether a snapshot was opened.',
dataType: 'boolean',
},
isTemplate: {
description: 'Whether the document is a template.',
dataType: 'boolean',
},
lastUpdated: {
description: 'Timestamp of when the document was last updated.',
dataType: 'date',
},
},
},
documentUsage: {
description: 'Triggered on doc open and close, as well as hourly while a document is open.',
minimumTelemetryLevel: Level.limited,
retentionPeriod: 'indefinitely',
metadataContracts: {
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
siteId: {
description: 'The site id.',
dataType: 'number',
minimumTelemetryLevel: Level.full,
},
siteType: {
description: 'The site type.',
dataType: 'string',
minimumTelemetryLevel: Level.full,
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
minimumTelemetryLevel: Level.full,
},
access: {
description: 'The document access level of the user that triggered this event.',
dataType: 'string',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
minimumTelemetryLevel: Level.full,
},
triggeredBy: {
description: 'What caused this event to trigger. May be either "docOpen", "interval", or "docClose".',
dataType: 'string',
},
isPublic: {
description: 'Whether the document is public.',
dataType: 'boolean',
},
rowCount: {
description: 'The number of rows in the document.',
dataType: 'number',
},
dataSizeBytes: {
description: 'The total size of all data in the document, excluding attachments.',
dataType: 'number',
},
attachmentsSize: {
description: 'The total size of all attachments in the document.',
dataType: 'number',
},
numAccessRules: {
description: 'The number of access rules in the document.',
dataType: 'number',
},
numUserAttributes: {
description: 'The number of user attributes in the document.',
dataType: 'number',
},
numAttachments: {
description: 'The number of attachments in the document.',
dataType: 'number',
},
attachmentTypes: {
description: "A list of unique file extensions compiled from all of the document's attachments.",
dataType: 'string[]',
},
numCharts: {
description: 'The number of charts in the document.',
dataType: 'number',
},
chartTypes: {
description: 'A list of chart types of every chart in the document.',
dataType: 'string[]',
},
numLinkedCharts: {
description: 'The number of linked charts in the document.',
dataType: 'number',
},
numLinkedWidgets: {
description: 'The number of linked widgets in the document.',
dataType: 'number',
},
numColumns: {
description: 'The number of columns in the document.',
dataType: 'number',
},
numColumnsWithConditionalFormatting: {
description: 'The number of columns with conditional formatting in the document.',
dataType: 'number',
},
numFormulaColumns: {
description: 'The number of formula columns in the document.',
dataType: 'number',
},
numTriggerFormulaColumns: {
description: 'The number of trigger formula columns in the document.',
dataType: 'number',
},
numSummaryFormulaColumns: {
description: 'The number of summary formula columns in the document.',
dataType: 'number',
},
numFieldsWithConditionalFormatting: {
description: 'The number of fields with conditional formatting in the document.',
dataType: 'number',
},
numTables: {
description: 'The number of tables in the document.',
dataType: 'number',
},
numOnDemandTables: {
description: 'The number of on-demand tables in the document.',
dataType: 'number',
},
numTablesWithConditionalFormatting: {
description: 'The number of tables with conditional formatting in the document.',
dataType: 'number',
},
numSummaryTables: {
description: 'The number of summary tables in the document.',
dataType: 'number',
},
numCustomWidgets: {
description: 'The number of custom widgets in the document.',
dataType: 'number',
},
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[]',
},
},
},
processMonitor: {
description: 'Triggered every 5 seconds.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
heapUsedMB: {
description: 'Size of JS heap in use, in MiB.',
dataType: 'number',
},
heapTotalMB: {
description: 'Total heap size, in MiB, allocated for JS by V8. ',
dataType: 'number',
},
cpuAverage: {
description: 'Fraction (typically between 0 and 1) of CPU usage. Includes all threads, so may exceed 1.',
dataType: 'number',
},
intervalMs: {
description: 'Interval (in milliseconds) over which `cpuAverage` is reported.',
dataType: 'number',
},
},
},
sendingWebhooks: {
description: 'Triggered when sending webhooks.',
minimumTelemetryLevel: Level.limited,
retentionPeriod: 'indefinitely',
metadataContracts: {
numEvents: {
description: 'The number of events in the batch of webhooks being sent.',
dataType: 'number',
},
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
siteId: {
description: 'The site id.',
dataType: 'number',
minimumTelemetryLevel: Level.full,
},
siteType: {
description: 'The site type.',
dataType: 'string',
minimumTelemetryLevel: Level.full,
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
minimumTelemetryLevel: Level.full,
},
access: {
description: 'The document access level of the user that triggered this event.',
dataType: 'string',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
minimumTelemetryLevel: Level.full,
},
},
},
signupFirstVisit: {
description: 'Triggered when a new user first opens the Grist app',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
siteId: {
description: 'The site id of first visit after signup.',
dataType: 'number',
},
siteType: {
description: 'The site type of first visit after signup.',
dataType: 'string',
},
userId: {
description: 'The id of the user that signed up.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
signupVerified: {
description: 'Triggered after a user successfully verifies their account during sign-up. '
+ 'Not triggered in grist-core.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
isAnonymousTemplateSignup: {
description: 'Whether the user viewed any templates before signing up.',
dataType: 'boolean',
},
templateId: {
description: 'The doc id of the template the user last viewed before signing up, if any.',
dataType: 'string',
},
},
},
siteMembership: {
description: 'Triggered daily.',
minimumTelemetryLevel: Level.limited,
retentionPeriod: 'indefinitely',
metadataContracts: {
siteId: {
description: 'The site id.',
dataType: 'number',
},
siteType: {
description: 'The site type.',
dataType: 'string',
},
numOwners: {
description: 'The number of users with an owner role in this site.',
dataType: 'number',
},
numEditors: {
description: 'The number of users with an editor role in this site.',
dataType: 'number',
},
numViewers: {
description: 'The number of users with a viewer role in this site.',
dataType: 'number',
},
},
},
siteUsage: {
description: 'Triggered daily.',
minimumTelemetryLevel: Level.limited,
retentionPeriod: 'indefinitely',
metadataContracts: {
siteId: {
description: 'The site id.',
dataType: 'number',
},
siteType: {
description: 'The site type.',
dataType: 'string',
},
inGoodStanding: {
description: "Whether the site's subscription is in good standing.",
dataType: 'boolean',
},
stripePlanId: {
description: 'The Stripe Plan id associated with this site.',
dataType: 'string',
minimumTelemetryLevel: Level.full,
},
numDocs: {
description: 'The number of docs in this site.',
dataType: 'number',
},
numWorkspaces: {
description: 'The number of workspaces in this site.',
dataType: 'number',
},
numMembers: {
description: 'The number of site members.',
dataType: 'number',
},
lastActivity: {
description: 'A timestamp of the most recent update made to a site document.',
dataType: 'date',
},
earliestDocCreatedAt: {
description: 'A timestamp of the earliest non-deleted document creation time.',
dataType: 'date',
},
},
},
tutorialProgressChanged: {
description: 'Triggered on changes to tutorial progress.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
tutorialForkIdDigest: {
description: 'A hash of the tutorial fork id.',
dataType: 'string',
},
tutorialTrunkIdDigest: {
description: 'A hash of the tutorial trunk id.',
dataType: 'string',
},
lastSlideIndex: {
description: 'The 0-based index of the last tutorial slide the user had open.',
dataType: 'number',
},
numSlides: {
description: 'The total number of slides in the tutorial.',
dataType: 'number',
},
percentComplete: {
description: 'Percentage of tutorial completion.',
dataType: 'number',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
},
},
},
tutorialRestarted: {
description: 'Triggered when a tutorial is restarted.',
minimumTelemetryLevel: Level.full,
retentionPeriod: 'indefinitely',
metadataContracts: {
tutorialForkIdDigest: {
description: 'A hash of the tutorial fork id.',
dataType: 'string',
},
tutorialTrunkIdDigest: {
description: 'A hash of the tutorial trunk id.',
dataType: 'string',
},
docIdDigest: {
description: 'A hash of the doc id.',
dataType: 'string',
},
siteId: {
description: 'The site id.',
dataType: 'number',
},
siteType: {
description: 'The site type.',
dataType: 'string',
},
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',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
},
},
},
watchedVideoTour: {
description: 'Triggered when the video tour is closed.',
minimumTelemetryLevel: Level.limited,
retentionPeriod: 'indefinitely',
metadataContracts: {
watchTimeSeconds: {
description: 'The number of seconds elapsed in the video player.',
dataType: 'number',
},
userId: {
description: 'The id of the user that triggered this event.',
dataType: 'number',
minimumTelemetryLevel: Level.full,
},
altSessionId: {
description: 'A random, session-based identifier for the user that triggered this event.',
dataType: 'string',
minimumTelemetryLevel: Level.full,
},
},
},
};
type TelemetryContracts = Record<TelemetryEvent, TelemetryEventContract>;
export const TelemetryEvents = StringUnion(
'apiUsage',
'assistantOpen',
'assistantSend',
'assistantReceive',
'assistantSave',
'assistantCancel',
'assistantClearConversation',
'assistantClose',
'beaconOpen',
'beaconArticleViewed',
'beaconEmailSent',
'beaconSearch',
'documentForked',
'documentOpened',
'documentUsage',
'processMonitor',
'sendingWebhooks',
'signupFirstVisit',
'signupVerified',
'siteMembership',
'siteUsage',
'tutorialProgressChanged',
'tutorialRestarted',
'watchedVideoTour',
);
export type TelemetryEvent = typeof TelemetryEvents.type;
interface TelemetryEventContract {
description: string;
minimumTelemetryLevel: Level;
retentionPeriod: TelemetryRetentionPeriod;
metadataContracts?: Record<string, MetadataContract>;
}
export type TelemetryRetentionPeriod = 'short' | 'indefinitely';
interface MetadataContract {
description: string;
dataType: 'boolean' | 'number' | 'string' | 'string[]' | 'date' | 'object';
minimumTelemetryLevel?: Level;
}
export type TelemetryMetadataByLevel = Partial<Record<EnabledTelemetryLevel, TelemetryMetadata>>;
export type EnabledTelemetryLevel = Exclude<TelemetryLevel, 'off'>;
export const TelemetryLevels = StringUnion('off', 'limited', 'full');
export type TelemetryLevel = typeof TelemetryLevels.type;
export type TelemetryMetadata = Record<string, any>;
/**
* The name of a cookie that's set whenever a template is opened.
*
* The cookie remembers the last template that was opened, which is then read during
* sign-up to track which templates were viewed before sign-up.
*/
export const TELEMETRY_TEMPLATE_SIGNUP_COOKIE_NAME = 'gr_template_signup_trk';
/**
* Returns a function that accepts a telemetry event and metadata, and performs various
* checks on it based on a set of contracts and the `telemetryLevel`.
*
* The function throws if any checks fail.
*/
export function buildTelemetryEventChecker(telemetryLevel: TelemetryLevel) {
const currentTelemetryLevel = Level[telemetryLevel];
return (event: TelemetryEvent, metadata?: TelemetryMetadata) => {
const eventContract = TelemetryContracts[event];
if (!eventContract) {
throw new Error(`Unknown telemetry event: ${event}`);
}
const eventMinimumTelemetryLevel = eventContract.minimumTelemetryLevel;
if (currentTelemetryLevel < eventMinimumTelemetryLevel) {
throw new Error(
`Telemetry event ${event} requires a minimum telemetry level of ${eventMinimumTelemetryLevel} ` +
`but the current level is ${currentTelemetryLevel}`
);
}
for (const [key, value] of Object.entries(metadata ?? {})) {
const metadataContract = eventContract.metadataContracts?.[key];
if (!metadataContract) {
throw new Error(`Unknown metadata for telemetry event ${event}: ${key}`);
}
const metadataMinimumTelemetryLevel = metadataContract.minimumTelemetryLevel;
if (metadataMinimumTelemetryLevel && currentTelemetryLevel < metadataMinimumTelemetryLevel) {
throw new Error(
`Telemetry metadata ${key} of event ${event} requires a minimum telemetry level of ` +
`${metadataMinimumTelemetryLevel} but the current level is ${currentTelemetryLevel}`
);
}
const {dataType} = metadataContract;
if (dataType.endsWith('[]')) {
if (!Array.isArray(value)) {
throw new Error(
`Telemetry metadata ${key} of event ${event} expected a value of type array ` +
`but received a value of type ${typeof value}`
);
}
const elementDataType = dataType.slice(0, -2);
if (value.some(element => typeof element !== elementDataType)) {
throw new Error(
`Telemetry metadata ${key} of event ${event} expected a value of type ${elementDataType}[] ` +
`but received a value of type ${typeof value}[]`
);
}
} else if (dataType === 'date') {
if (!(value instanceof Date) && typeof value !== 'string') {
throw new Error(
`Telemetry metadata ${key} of event ${event} expected a value of type Date or string ` +
`but received a value of type ${typeof value}`
);
}
if (typeof value === 'string' && !hasTimezone(value)) {
throw new Error(
`Telemetry metadata ${key} of event ${event} has an ambiguous date string`
);
}
} else if (dataType !== typeof value) {
throw new Error(
`Telemetry metadata ${key} of event ${event} expected a value of type ${dataType} ` +
`but received a value of type ${typeof value}`
);
}
}
};
}
// Check that datetime looks like it has a timezone in it. If not,
// that could be a problem for whatever ingests the data.
function hasTimezone(isoDateString: string) {
// Use a regular expression to check for a timezone offset or 'Z'
return /([+-]\d{2}:\d{2}|Z)$/.test(isoDateString);
}
export type TelemetryEventChecker = (event: TelemetryEvent, metadata?: TelemetryMetadata) => void;