n8n/packages/cli/src/events/telemetry-event-relay.ts

886 lines
26 KiB
TypeScript

import { Service } from 'typedi';
import { EventService } from '@/events/event.service';
import type { RelayEventMap } from '@/events/relay-event-map';
import { Telemetry } from '../telemetry';
import config from '@/config';
import os from 'node:os';
import { License } from '@/License';
import { GlobalConfig } from '@n8n/config';
import { N8N_VERSION } from '@/constants';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import type { ExecutionStatus, INodesGraphResult, ITelemetryTrackProperties } from 'n8n-workflow';
import { get as pslGet } from 'psl';
import { TelemetryHelpers } from 'n8n-workflow';
import { NodeTypes } from '@/NodeTypes';
import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository';
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
import type { IExecutionTrackProperties } from '@/Interfaces';
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
import { EventRelay } from './event-relay';
import { snakeCase } from 'change-case';
@Service()
export class TelemetryEventRelay extends EventRelay {
constructor(
readonly eventService: EventService,
private readonly telemetry: Telemetry,
private readonly license: License,
private readonly globalConfig: GlobalConfig,
private readonly workflowRepository: WorkflowRepository,
private readonly nodeTypes: NodeTypes,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly projectRelationRepository: ProjectRelationRepository,
) {
super(eventService);
}
async init() {
if (!config.getEnv('diagnostics.enabled')) return;
await this.telemetry.init();
this.setupListeners({
'team-project-updated': (event) => this.teamProjectUpdated(event),
'team-project-deleted': (event) => this.teamProjectDeleted(event),
'team-project-created': (event) => this.teamProjectCreated(event),
'source-control-settings-updated': (event) => this.sourceControlSettingsUpdated(event),
'source-control-user-started-pull-ui': (event) => this.sourceControlUserStartedPullUi(event),
'source-control-user-finished-pull-ui': (event) =>
this.sourceControlUserFinishedPullUi(event),
'source-control-user-pulled-api': (event) => this.sourceControlUserPulledApi(event),
'source-control-user-started-push-ui': (event) => this.sourceControlUserStartedPushUi(event),
'source-control-user-finished-push-ui': (event) =>
this.sourceControlUserFinishedPushUi(event),
'license-renewal-attempted': (event) => this.licenseRenewalAttempted(event),
'variable-created': () => this.variableCreated(),
'external-secrets-provider-settings-saved': (event) =>
this.externalSecretsProviderSettingsSaved(event),
'public-api-invoked': (event) => this.publicApiInvoked(event),
'public-api-key-created': (event) => this.publicApiKeyCreated(event),
'public-api-key-deleted': (event) => this.publicApiKeyDeleted(event),
'community-package-installed': (event) => this.communityPackageInstalled(event),
'community-package-updated': (event) => this.communityPackageUpdated(event),
'community-package-deleted': (event) => this.communityPackageDeleted(event),
'credentials-created': (event) => this.credentialsCreated(event),
'credentials-shared': (event) => this.credentialsShared(event),
'credentials-updated': (event) => this.credentialsUpdated(event),
'credentials-deleted': (event) => this.credentialsDeleted(event),
'ldap-general-sync-finished': (event) => this.ldapGeneralSyncFinished(event),
'ldap-settings-updated': (event) => this.ldapSettingsUpdated(event),
'ldap-login-sync-failed': (event) => this.ldapLoginSyncFailed(event),
'login-failed-due-to-ldap-disabled': (event) => this.loginFailedDueToLdapDisabled(event),
'workflow-created': (event) => this.workflowCreated(event),
'workflow-deleted': (event) => this.workflowDeleted(event),
'workflow-saved': async (event) => await this.workflowSaved(event),
'server-started': async () => await this.serverStarted(),
'workflow-post-execute': async (event) => await this.workflowPostExecute(event),
'user-changed-role': (event) => this.userChangedRole(event),
'user-retrieved-user': (event) => this.userRetrievedUser(event),
'user-retrieved-all-users': (event) => this.userRetrievedAllUsers(event),
'user-retrieved-execution': (event) => this.userRetrievedExecution(event),
'user-retrieved-all-executions': (event) => this.userRetrievedAllExecutions(event),
'user-retrieved-workflow': (event) => this.userRetrievedWorkflow(event),
'user-retrieved-all-workflows': (event) => this.userRetrievedAllWorkflows(event),
'user-updated': (event) => this.userUpdated(event),
'user-deleted': (event) => this.userDeleted(event),
'user-invited': (event) => this.userInvited(event),
'user-signed-up': (event) => this.userSignedUp(event),
'user-submitted-personalization-survey': (event) =>
this.userSubmittedPersonalizationSurvey(event),
});
}
// #endregion
// #region Team
private teamProjectUpdated({
userId,
role,
members,
projectId,
}: RelayEventMap['team-project-updated']) {
this.telemetry.track('Project settings updated', {
user_id: userId,
role,
// eslint-disable-next-line @typescript-eslint/no-shadow
members: members.map(({ userId: user_id, role }) => ({ user_id, role })),
project_id: projectId,
});
}
private teamProjectDeleted({
userId,
role,
projectId,
removalType,
targetProjectId,
}: RelayEventMap['team-project-deleted']) {
this.telemetry.track('User deleted project', {
user_id: userId,
role,
project_id: projectId,
removal_type: removalType,
target_project_id: targetProjectId,
});
}
private teamProjectCreated({ userId, role }: RelayEventMap['team-project-created']) {
this.telemetry.track('User created project', {
user_id: userId,
role,
});
}
// #endregion
// #region Source control
private sourceControlSettingsUpdated({
branchName,
readOnlyInstance,
repoType,
connected,
}: RelayEventMap['source-control-settings-updated']) {
this.telemetry.track('User updated source control settings', {
branch_name: branchName,
read_only_instance: readOnlyInstance,
repo_type: repoType,
connected,
});
}
private sourceControlUserStartedPullUi({
workflowUpdates,
workflowConflicts,
credConflicts,
}: RelayEventMap['source-control-user-started-pull-ui']) {
this.telemetry.track('User started pull via UI', {
workflow_updates: workflowUpdates,
workflow_conflicts: workflowConflicts,
cred_conflicts: credConflicts,
});
}
private sourceControlUserFinishedPullUi({
workflowUpdates,
}: RelayEventMap['source-control-user-finished-pull-ui']) {
this.telemetry.track('User finished pull via UI', {
workflow_updates: workflowUpdates,
});
}
private sourceControlUserPulledApi({
workflowUpdates,
forced,
}: RelayEventMap['source-control-user-pulled-api']) {
this.telemetry.track('User pulled via API', {
workflow_updates: workflowUpdates,
forced,
});
}
private sourceControlUserStartedPushUi({
workflowsEligible,
workflowsEligibleWithConflicts,
credsEligible,
credsEligibleWithConflicts,
variablesEligible,
}: RelayEventMap['source-control-user-started-push-ui']) {
this.telemetry.track('User started push via UI', {
workflows_eligible: workflowsEligible,
workflows_eligible_with_conflicts: workflowsEligibleWithConflicts,
creds_eligible: credsEligible,
creds_eligible_with_conflicts: credsEligibleWithConflicts,
variables_eligible: variablesEligible,
});
}
private sourceControlUserFinishedPushUi({
workflowsEligible,
workflowsPushed,
credsPushed,
variablesPushed,
}: RelayEventMap['source-control-user-finished-push-ui']) {
this.telemetry.track('User finished push via UI', {
workflows_eligible: workflowsEligible,
workflows_pushed: workflowsPushed,
creds_pushed: credsPushed,
variables_pushed: variablesPushed,
});
}
// #endregion
// #region License
private licenseRenewalAttempted({ success }: RelayEventMap['license-renewal-attempted']) {
this.telemetry.track('Instance attempted to refresh license', {
success,
});
}
// #endregion
// #region Variable
private variableCreated() {
this.telemetry.track('User created variable');
}
// #endregion
// #region External secrets
private externalSecretsProviderSettingsSaved({
userId,
vaultType,
isValid,
isNew,
errorMessage,
}: RelayEventMap['external-secrets-provider-settings-saved']) {
this.telemetry.track('User updated external secrets settings', {
user_id: userId,
vault_type: vaultType,
is_valid: isValid,
is_new: isNew,
error_message: errorMessage,
});
}
// #endregion
// #region Public API
private publicApiInvoked({
userId,
path,
method,
apiVersion,
}: RelayEventMap['public-api-invoked']) {
this.telemetry.track('User invoked API', {
user_id: userId,
path,
method,
api_version: apiVersion,
});
}
private publicApiKeyCreated(event: RelayEventMap['public-api-key-created']) {
const { user, publicApi } = event;
this.telemetry.track('API key created', {
user_id: user.id,
public_api: publicApi,
});
}
private publicApiKeyDeleted(event: RelayEventMap['public-api-key-deleted']) {
const { user, publicApi } = event;
this.telemetry.track('API key deleted', {
user_id: user.id,
public_api: publicApi,
});
}
// #endregion
// #region Community package
private communityPackageInstalled({
user,
inputString,
packageName,
success,
packageVersion,
packageNodeNames,
packageAuthor,
packageAuthorEmail,
failureReason,
}: RelayEventMap['community-package-installed']) {
this.telemetry.track('cnr package install finished', {
user_id: user.id,
input_string: inputString,
package_name: packageName,
success,
package_version: packageVersion,
package_node_names: packageNodeNames,
package_author: packageAuthor,
package_author_email: packageAuthorEmail,
failure_reason: failureReason,
});
}
private communityPackageUpdated({
user,
packageName,
packageVersionCurrent,
packageVersionNew,
packageNodeNames,
packageAuthor,
packageAuthorEmail,
}: RelayEventMap['community-package-updated']) {
this.telemetry.track('cnr package updated', {
user_id: user.id,
package_name: packageName,
package_version_current: packageVersionCurrent,
package_version_new: packageVersionNew,
package_node_names: packageNodeNames,
package_author: packageAuthor,
package_author_email: packageAuthorEmail,
});
}
private communityPackageDeleted({
user,
packageName,
packageVersion,
packageNodeNames,
packageAuthor,
packageAuthorEmail,
}: RelayEventMap['community-package-deleted']) {
this.telemetry.track('cnr package deleted', {
user_id: user.id,
package_name: packageName,
package_version: packageVersion,
package_node_names: packageNodeNames,
package_author: packageAuthor,
package_author_email: packageAuthorEmail,
});
}
// #endregion
// #region Credentials
private credentialsCreated({
user,
credentialType,
credentialId,
projectId,
projectType,
}: RelayEventMap['credentials-created']) {
this.telemetry.track('User created credentials', {
user_id: user.id,
credential_type: credentialType,
credential_id: credentialId,
project_id: projectId,
project_type: projectType,
});
}
private credentialsShared({
user,
credentialType,
credentialId,
userIdSharer,
userIdsShareesAdded,
shareesRemoved,
}: RelayEventMap['credentials-shared']) {
this.telemetry.track('User updated cred sharing', {
user_id: user.id,
credential_type: credentialType,
credential_id: credentialId,
user_id_sharer: userIdSharer,
user_ids_sharees_added: userIdsShareesAdded,
sharees_removed: shareesRemoved,
});
}
private credentialsUpdated({
user,
credentialId,
credentialType,
}: RelayEventMap['credentials-updated']) {
this.telemetry.track('User updated credentials', {
user_id: user.id,
credential_type: credentialType,
credential_id: credentialId,
});
}
private credentialsDeleted({
user,
credentialId,
credentialType,
}: RelayEventMap['credentials-deleted']) {
this.telemetry.track('User deleted credentials', {
user_id: user.id,
credential_type: credentialType,
credential_id: credentialId,
});
}
// #endregion
// #region LDAP
private ldapGeneralSyncFinished({
type,
succeeded,
usersSynced,
error,
}: RelayEventMap['ldap-general-sync-finished']) {
this.telemetry.track('Ldap general sync finished', {
type,
succeeded,
users_synced: usersSynced,
error,
});
}
private ldapSettingsUpdated({
userId,
loginIdAttribute,
firstNameAttribute,
lastNameAttribute,
emailAttribute,
ldapIdAttribute,
searchPageSize,
searchTimeout,
synchronizationEnabled,
synchronizationInterval,
loginLabel,
loginEnabled,
}: RelayEventMap['ldap-settings-updated']) {
this.telemetry.track('User updated Ldap settings', {
user_id: userId,
loginIdAttribute,
firstNameAttribute,
lastNameAttribute,
emailAttribute,
ldapIdAttribute,
searchPageSize,
searchTimeout,
synchronizationEnabled,
synchronizationInterval,
loginLabel,
loginEnabled,
});
}
private ldapLoginSyncFailed({ error }: RelayEventMap['ldap-login-sync-failed']) {
this.telemetry.track('Ldap login sync failed', { error });
}
private loginFailedDueToLdapDisabled({
userId,
}: RelayEventMap['login-failed-due-to-ldap-disabled']) {
this.telemetry.track('User login failed since ldap disabled', { user_ud: userId });
}
// #endregion
// #region Workflow
private workflowCreated({
user,
workflow,
publicApi,
projectId,
projectType,
}: RelayEventMap['workflow-created']) {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
this.telemetry.track('User created workflow', {
user_id: user.id,
workflow_id: workflow.id,
node_graph_string: JSON.stringify(nodeGraph),
public_api: publicApi,
project_id: projectId,
project_type: projectType,
});
}
private workflowDeleted({ user, workflowId, publicApi }: RelayEventMap['workflow-deleted']) {
this.telemetry.track('User deleted workflow', {
user_id: user.id,
workflow_id: workflowId,
public_api: publicApi,
});
}
private async workflowSaved({ user, workflow, publicApi }: RelayEventMap['workflow-saved']) {
const isCloudDeployment = config.getEnv('deployment.type') === 'cloud';
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes, {
isCloudDeployment,
});
let userRole: 'owner' | 'sharee' | 'member' | undefined = undefined;
const role = await this.sharedWorkflowRepository.findSharingRole(user.id, workflow.id);
if (role) {
userRole = role === 'workflow:owner' ? 'owner' : 'sharee';
} else {
const workflowOwner = await this.sharedWorkflowRepository.getWorkflowOwningProject(
workflow.id,
);
if (workflowOwner) {
const projectRole = await this.projectRelationRepository.findProjectRole({
userId: user.id,
projectId: workflowOwner.id,
});
if (projectRole && projectRole !== 'project:personalOwner') {
userRole = 'member';
}
}
}
const notesCount = Object.keys(nodeGraph.notes).length;
const overlappingCount = Object.values(nodeGraph.notes).filter(
(note) => note.overlapping,
).length;
this.telemetry.track('User saved workflow', {
user_id: user.id,
workflow_id: workflow.id,
node_graph_string: JSON.stringify(nodeGraph),
notes_count_overlapping: overlappingCount,
notes_count_non_overlapping: notesCount - overlappingCount,
version_cli: N8N_VERSION,
num_tags: workflow.tags?.length ?? 0,
public_api: publicApi,
sharing_role: userRole,
});
}
// eslint-disable-next-line complexity
private async workflowPostExecute({
workflow,
runData,
userId,
}: RelayEventMap['workflow-post-execute']) {
if (!workflow.id) {
return;
}
if (runData?.status === 'waiting') {
// No need to send telemetry or logs when the workflow hasn't finished yet.
return;
}
const telemetryProperties: IExecutionTrackProperties = {
workflow_id: workflow.id,
is_manual: false,
version_cli: N8N_VERSION,
success: false,
};
if (userId) {
telemetryProperties.user_id = userId;
}
if (runData?.data.resultData.error?.message?.includes('canceled')) {
runData.status = 'canceled';
}
telemetryProperties.success = !!runData?.finished;
// const executionStatus: ExecutionStatus = runData?.status ?? 'unknown';
const executionStatus: ExecutionStatus = runData
? determineFinalExecutionStatus(runData)
: 'unknown';
if (runData !== undefined) {
telemetryProperties.execution_mode = runData.mode;
telemetryProperties.is_manual = runData.mode === 'manual';
let nodeGraphResult: INodesGraphResult | null = null;
if (!telemetryProperties.success && runData?.data.resultData.error) {
telemetryProperties.error_message = runData?.data.resultData.error.message;
let errorNodeName =
'node' in runData?.data.resultData.error
? runData?.data.resultData.error.node?.name
: undefined;
telemetryProperties.error_node_type =
'node' in runData?.data.resultData.error
? runData?.data.resultData.error.node?.type
: undefined;
if (runData.data.resultData.lastNodeExecuted) {
const lastNode = TelemetryHelpers.getNodeTypeForName(
workflow,
runData.data.resultData.lastNodeExecuted,
);
if (lastNode !== undefined) {
telemetryProperties.error_node_type = lastNode.type;
errorNodeName = lastNode.name;
}
}
if (telemetryProperties.is_manual) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
telemetryProperties.node_graph = nodeGraphResult.nodeGraph;
telemetryProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
if (errorNodeName) {
telemetryProperties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
}
}
}
if (telemetryProperties.is_manual) {
if (!nodeGraphResult) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
}
let userRole: 'owner' | 'sharee' | undefined = undefined;
if (userId) {
const role = await this.sharedWorkflowRepository.findSharingRole(userId, workflow.id);
if (role) {
userRole = role === 'workflow:owner' ? 'owner' : 'sharee';
}
}
const manualExecEventProperties: ITelemetryTrackProperties = {
user_id: userId,
workflow_id: workflow.id,
status: executionStatus,
executionStatus: runData?.status ?? 'unknown',
error_message: telemetryProperties.error_message as string,
error_node_type: telemetryProperties.error_node_type,
node_graph_string: telemetryProperties.node_graph_string as string,
error_node_id: telemetryProperties.error_node_id as string,
webhook_domain: null,
sharing_role: userRole,
};
if (!manualExecEventProperties.node_graph_string) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
manualExecEventProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
}
if (runData.data.startData?.destinationNode) {
const telemetryPayload = {
...manualExecEventProperties,
node_type: TelemetryHelpers.getNodeTypeForName(
workflow,
runData.data.startData?.destinationNode,
)?.type,
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode],
};
this.telemetry.track('Manual node exec finished', telemetryPayload);
} else {
nodeGraphResult.webhookNodeNames.forEach((name: string) => {
const execJson = runData.data.resultData.runData[name]?.[0]?.data?.main?.[0]?.[0]
?.json as { headers?: { origin?: string } };
if (execJson?.headers?.origin && execJson.headers.origin !== '') {
manualExecEventProperties.webhook_domain = pslGet(
execJson.headers.origin.replace(/^https?:\/\//, ''),
);
}
});
this.telemetry.track('Manual workflow exec finished', manualExecEventProperties);
}
}
}
this.telemetry.trackWorkflowExecution(telemetryProperties);
}
// #endregion
// #region Server
private async serverStarted() {
const cpus = os.cpus();
const binaryDataConfig = config.getEnv('binaryDataManager');
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
const isS3Licensed = this.license.isBinaryDataS3Licensed();
const authenticationMethod = config.getEnv('userManagement.authenticationMethod');
const info = {
version_cli: N8N_VERSION,
db_type: this.globalConfig.database.type,
n8n_version_notifications_enabled: this.globalConfig.versionNotifications.enabled,
n8n_disable_production_main_process:
this.globalConfig.endpoints.disableProductionWebhooksOnMainProcess,
system_info: {
os: {
type: os.type(),
version: os.version(),
},
memory: os.totalmem() / 1024,
cpus: {
count: cpus.length,
model: cpus[0].model,
speed: cpus[0].speed,
},
},
execution_variables: {
executions_mode: config.getEnv('executions.mode'),
executions_timeout: config.getEnv('executions.timeout'),
executions_timeout_max: config.getEnv('executions.maxTimeout'),
executions_data_save_on_error: config.getEnv('executions.saveDataOnError'),
executions_data_save_on_success: config.getEnv('executions.saveDataOnSuccess'),
executions_data_save_on_progress: config.getEnv('executions.saveExecutionProgress'),
executions_data_save_manual_executions: config.getEnv(
'executions.saveDataManualExecutions',
),
executions_data_prune: config.getEnv('executions.pruneData'),
executions_data_max_age: config.getEnv('executions.pruneDataMaxAge'),
},
n8n_deployment_type: config.getEnv('deployment.type'),
n8n_binary_data_mode: binaryDataConfig.mode,
smtp_set_up: this.globalConfig.userManagement.emails.mode === 'smtp',
ldap_allowed: authenticationMethod === 'ldap',
saml_enabled: authenticationMethod === 'saml',
license_plan_name: this.license.getPlanName(),
license_tenant_id: config.getEnv('license.tenantId'),
binary_data_s3: isS3Available && isS3Selected && isS3Licensed,
multi_main_setup_enabled: config.getEnv('multiMainSetup.enabled'),
};
const firstWorkflow = await this.workflowRepository.findOne({
select: ['createdAt'],
order: { createdAt: 'ASC' },
where: {},
});
this.telemetry.identify(info);
this.telemetry.track('Instance started', {
...info,
earliest_workflow_created: firstWorkflow?.createdAt,
});
}
// #endregion
// #region User
private userChangedRole({
userId,
targetUserId,
targetUserNewRole,
publicApi,
}: RelayEventMap['user-changed-role']) {
this.telemetry.track('User changed role', {
user_id: userId,
target_user_id: targetUserId,
target_user_new_role: targetUserNewRole,
public_api: publicApi,
});
}
private userRetrievedUser({ userId, publicApi }: RelayEventMap['user-retrieved-user']) {
this.telemetry.track('User retrieved user', {
user_id: userId,
public_api: publicApi,
});
}
private userRetrievedAllUsers({ userId, publicApi }: RelayEventMap['user-retrieved-all-users']) {
this.telemetry.track('User retrieved all users', {
user_id: userId,
public_api: publicApi,
});
}
private userRetrievedExecution({ userId, publicApi }: RelayEventMap['user-retrieved-execution']) {
this.telemetry.track('User retrieved execution', {
user_id: userId,
public_api: publicApi,
});
}
private userRetrievedAllExecutions({
userId,
publicApi,
}: RelayEventMap['user-retrieved-all-executions']) {
this.telemetry.track('User retrieved all executions', {
user_id: userId,
public_api: publicApi,
});
}
private userRetrievedWorkflow({ userId, publicApi }: RelayEventMap['user-retrieved-workflow']) {
this.telemetry.track('User retrieved workflow', {
user_id: userId,
public_api: publicApi,
});
}
private userRetrievedAllWorkflows({
userId,
publicApi,
}: RelayEventMap['user-retrieved-all-workflows']) {
this.telemetry.track('User retrieved all workflows', {
user_id: userId,
public_api: publicApi,
});
}
private userUpdated({ user, fieldsChanged }: RelayEventMap['user-updated']) {
this.telemetry.track('User changed personal settings', {
user_id: user.id,
fields_changed: fieldsChanged,
});
}
private userDeleted({
user,
publicApi,
targetUserOldStatus,
migrationStrategy,
targetUserId,
migrationUserId,
}: RelayEventMap['user-deleted']) {
this.telemetry.track('User deleted user', {
user_id: user.id,
public_api: publicApi,
target_user_old_status: targetUserOldStatus,
migration_strategy: migrationStrategy,
target_user_id: targetUserId,
migration_user_id: migrationUserId,
});
}
private userInvited({
user,
targetUserId,
publicApi,
emailSent,
inviteeRole,
}: RelayEventMap['user-invited']) {
this.telemetry.track('User invited new user', {
user_id: user.id,
target_user_id: targetUserId,
public_api: publicApi,
email_sent: emailSent,
invitee_role: inviteeRole,
});
}
private userSignedUp({ user, userType, wasDisabledLdapUser }: RelayEventMap['user-signed-up']) {
this.telemetry.track('User signed up', {
user_id: user.id,
user_type: userType,
was_disabled_ldap_user: wasDisabledLdapUser,
});
}
private userSubmittedPersonalizationSurvey({
userId,
answers,
}: RelayEventMap['user-submitted-personalization-survey']) {
const camelCaseKeys = Object.keys(answers);
const personalizationSurveyData = { user_id: userId } as Record<string, string | string[]>;
camelCaseKeys.forEach((camelCaseKey) => {
personalizationSurveyData[snakeCase(camelCaseKey)] = answers[camelCaseKey];
});
this.telemetry.track('User responded to personalization questions', personalizationSurveyData);
}
// #endregion
}