refactor(core): Decouple user events from internal hooks (no-changelog) (#10292)

This commit is contained in:
Iván Ovejero 2024-08-05 12:07:42 +02:00 committed by GitHub
parent 88086a41ff
commit c0f3693e8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 260 additions and 181 deletions

View file

@ -267,14 +267,6 @@ export interface IWebhookManager {
executeWebhook(req: WebhookRequest, res: Response): Promise<IResponseCallbackData>;
}
export interface ITelemetryUserDeletionData {
user_id: string;
target_user_old_status: 'active' | 'invited';
migration_strategy?: 'transfer_data' | 'delete_data';
target_user_id?: string;
migration_user_id?: string;
}
export interface IVersionNotificationSettings {
enabled: boolean;
endpoint: string;

View file

@ -1,9 +1,6 @@
import { Service } from 'typedi';
import { snakeCase } from 'change-case';
import type { ITelemetryTrackProperties } from 'n8n-workflow';
import type { AuthProviderType } from '@db/entities/AuthIdentity';
import type { User } from '@db/entities/User';
import type { ITelemetryUserDeletionData } from '@/Interfaces';
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
import { Telemetry } from '@/telemetry';
import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
@ -39,16 +36,6 @@ export class InternalHooks {
this.telemetry.track('Session started', { session_id: pushRef });
}
onPersonalizationSurveySubmitted(userId: string, answers: Record<string, string>): void {
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);
}
onWorkflowSharingUpdate(workflowId: string, userId: string, userList: string[]) {
const properties: ITelemetryTrackProperties = {
workflow_id: workflowId,
@ -67,76 +54,6 @@ export class InternalHooks {
return await Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
}
onUserDeletion(userDeletionData: {
user: User;
telemetryData: ITelemetryUserDeletionData;
publicApi: boolean;
}) {
this.telemetry.track('User deleted user', {
...userDeletionData.telemetryData,
user_id: userDeletionData.user.id,
public_api: userDeletionData.publicApi,
});
}
onUserInvite(userInviteData: {
user: User;
target_user_id: string[];
public_api: boolean;
email_sent: boolean;
invitee_role: string;
}) {
this.telemetry.track('User invited new user', {
user_id: userInviteData.user.id,
target_user_id: userInviteData.target_user_id,
public_api: userInviteData.public_api,
email_sent: userInviteData.email_sent,
invitee_role: userInviteData.invitee_role,
});
}
onUserRoleChange(userRoleChangeData: {
user: User;
target_user_id: string;
public_api: boolean;
target_user_new_role: string;
}) {
const { user, ...rest } = userRoleChangeData;
this.telemetry.track('User changed role', { user_id: user.id, ...rest });
}
onUserRetrievedUser(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved user', userRetrievedData);
}
onUserRetrievedAllUsers(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved all users', userRetrievedData);
}
onUserRetrievedExecution(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved execution', userRetrievedData);
}
onUserRetrievedAllExecutions(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved all executions', userRetrievedData);
}
onUserRetrievedWorkflow(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved workflow', userRetrievedData);
}
onUserRetrievedAllWorkflows(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved all workflows', userRetrievedData);
}
onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }) {
this.telemetry.track('User changed personal settings', {
user_id: userUpdateData.user.id,
fields_changed: userUpdateData.fields_changed,
});
}
onUserInviteEmailClick(userInviteClickData: { inviter: User; invitee: User }) {
this.telemetry.track('User clicked invite link from email', {
user_id: userInviteClickData.invitee.id,
@ -172,19 +89,6 @@ export class InternalHooks {
this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
}
onUserSignup(
user: User,
userSignupData: {
user_type: AuthProviderType;
was_disabled_ldap_user: boolean;
},
) {
this.telemetry.track('User signed up', {
user_id: user.id,
...userSignupData,
});
}
onEmailFailed(failedEmailData: {
user: User;
message_type:

View file

@ -7,7 +7,7 @@ import { validCursor } from '../../shared/middlewares/global.middleware';
import type { ExecutionRequest } from '../../../types';
import { getSharedWorkflowIds } from '../workflows/workflows.service';
import { encodeNextCursor } from '../../shared/services/pagination.service';
import { InternalHooks } from '@/InternalHooks';
import { EventService } from '@/events/event.service';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service';
@ -78,9 +78,9 @@ export = {
return res.status(404).json({ message: 'Not Found' });
}
Container.get(InternalHooks).onUserRetrievedExecution({
user_id: req.user.id,
public_api: true,
Container.get(EventService).emit('user-retrieved-execution', {
userId: req.user.id,
publicApi: true,
});
return res.json(replaceCircularReferences(execution));
@ -130,9 +130,9 @@ export = {
const count =
await Container.get(ExecutionRepository).getExecutionsCountForPublicApi(filters);
Container.get(InternalHooks).onUserRetrievedAllExecutions({
user_id: req.user.id,
public_api: true,
Container.get(EventService).emit('user-retrieved-all-executions', {
userId: req.user.id,
publicApi: true,
});
return res.json({

View file

@ -11,7 +11,7 @@ import {
validLicenseWithUserQuota,
} from '../../shared/middlewares/global.middleware';
import type { UserRequest } from '@/requests';
import { InternalHooks } from '@/InternalHooks';
import { EventService } from '@/events/event.service';
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
import type { Response } from 'express';
import { InvitationController } from '@/controllers/invitation.controller';
@ -37,12 +37,10 @@ export = {
});
}
const telemetryData = {
user_id: req.user.id,
public_api: true,
};
Container.get(InternalHooks).onUserRetrievedUser(telemetryData);
Container.get(EventService).emit('user-retrieved-user', {
userId: req.user.id,
publicApi: true,
});
return res.json(clean(user, { includeRole }));
},
@ -65,12 +63,10 @@ export = {
in: _in,
});
const telemetryData = {
user_id: req.user.id,
public_api: true,
};
Container.get(InternalHooks).onUserRetrievedAllUsers(telemetryData);
Container.get(EventService).emit('user-retrieved-all-users', {
userId: req.user.id,
publicApi: true,
});
return res.json({
data: clean(users, { includeRole }),

View file

@ -26,7 +26,6 @@ import {
updateTags,
} from './workflows.service';
import { WorkflowService } from '@/workflows/workflow.service';
import { InternalHooks } from '@/InternalHooks';
import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee';
import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository';
import { TagRepository } from '@/databases/repositories/tag.repository';
@ -119,9 +118,9 @@ export = {
return res.status(404).json({ message: 'Not Found' });
}
Container.get(InternalHooks).onUserRetrievedWorkflow({
user_id: req.user.id,
public_api: true,
Container.get(EventService).emit('user-retrieved-workflow', {
userId: req.user.id,
publicApi: true,
});
return res.json(workflow);
@ -185,9 +184,9 @@ export = {
...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }),
});
Container.get(InternalHooks).onUserRetrievedAllWorkflows({
user_id: req.user.id,
public_api: true,
Container.get(EventService).emit('user-retrieved-all-workflows', {
userId: req.user.id,
publicApi: true,
});
return res.json({

View file

@ -1,6 +1,5 @@
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
import { LdapService } from '@/Ldap/ldap.service.ee';
import {
createLdapUserOnLocalDb,
@ -51,11 +50,11 @@ export const handleLdapLogin = async (
await updateLdapUserOnLocalDb(identity, ldapAttributesValues);
} else {
const user = await createLdapUserOnLocalDb(ldapAttributesValues, ldapId);
Container.get(InternalHooks).onUserSignup(user, {
user_type: 'ldap',
was_disabled_ldap_user: false,
Container.get(EventService).emit('user-signed-up', {
user,
userType: 'ldap',
wasDisabledLdapUser: false,
});
Container.get(EventService).emit('user-signed-up', { user });
return user;
}
} else {

View file

@ -13,6 +13,7 @@ import { InternalHooks } from '@/InternalHooks';
import { License } from '@/License';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UserRepository } from '@/databases/repositories/user.repository';
import { EventService } from '@/events/event.service';
import { badPasswords } from '@test/testData';
import { mockInstance } from '@test/mocking';
@ -20,7 +21,8 @@ const browserId = 'test-browser-id';
describe('MeController', () => {
const externalHooks = mockInstance(ExternalHooks);
const internalHooks = mockInstance(InternalHooks);
mockInstance(InternalHooks);
const eventService = mockInstance(EventService);
const userService = mockInstance(UserService);
const userRepository = mockInstance(UserRepository);
mockInstance(License).isWithinUsersLimit.mockReturnValue(true);
@ -202,9 +204,9 @@ describe('MeController', () => {
req.user.password,
]);
expect(internalHooks.onUserUpdate).toHaveBeenCalledWith({
expect(eventService.emit).toHaveBeenCalledWith('user-updated', {
user: req.user,
fields_changed: ['password'],
fieldsChanged: ['password'],
});
});
});

View file

@ -16,7 +16,6 @@ import type { User } from '@/databases/entities/User';
import { UserRepository } from '@db/repositories/user.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { InternalHooks } from '@/InternalHooks';
import { ExternalHooks } from '@/ExternalHooks';
import { EventService } from '@/events/event.service';
@ -24,7 +23,6 @@ import { EventService } from '@/events/event.service';
export class InvitationController {
constructor(
private readonly logger: Logger,
private readonly internalHooks: InternalHooks,
private readonly externalHooks: ExternalHooks,
private readonly authService: AuthService,
private readonly userService: UserService,
@ -168,11 +166,11 @@ export class InvitationController {
this.authService.issueCookie(res, updatedUser, req.browserId);
this.internalHooks.onUserSignup(updatedUser, {
user_type: 'email',
was_disabled_ldap_user: false,
this.eventService.emit('user-signed-up', {
user: updatedUser,
userType: 'email',
wasDisabledLdapUser: false,
});
this.eventService.emit('user-signed-up', { user: updatedUser });
const publicInvitee = await this.userService.toPublic(invitee);

View file

@ -19,7 +19,6 @@ import { isSamlLicensedAndEnabled } from '@/sso/saml/samlHelpers';
import { UserService } from '@/services/user.service';
import { Logger } from '@/Logger';
import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UserRepository } from '@/databases/repositories/user.repository';
import { isApiEnabled } from '@/PublicApi';
@ -40,7 +39,6 @@ export class MeController {
constructor(
private readonly logger: Logger,
private readonly externalHooks: ExternalHooks,
private readonly internalHooks: InternalHooks,
private readonly authService: AuthService,
private readonly userService: UserService,
private readonly passwordUtility: PasswordUtility,
@ -101,7 +99,6 @@ export class MeController {
this.authService.issueCookie(res, user, req.browserId);
const fieldsChanged = Object.keys(payload);
this.internalHooks.onUserUpdate({ user, fields_changed: fieldsChanged });
this.eventService.emit('user-updated', { user, fieldsChanged });
const publicUser = await this.userService.toPublic(user);
@ -151,7 +148,6 @@ export class MeController {
this.authService.issueCookie(res, updatedUser, req.browserId);
this.internalHooks.onUserUpdate({ user: updatedUser, fields_changed: ['password'] });
this.eventService.emit('user-updated', { user: updatedUser, fieldsChanged: ['password'] });
await this.externalHooks.run('user.password.update', [updatedUser.email, updatedUser.password]);
@ -186,7 +182,10 @@ export class MeController {
this.logger.info('User survey updated successfully', { userId: req.user.id });
this.internalHooks.onPersonalizationSurveySubmitted(req.user.id, personalizationAnswers);
this.eventService.emit('user-submitted-personalization-survey', {
userId: req.user.id,
answers: personalizationAnswers,
});
return { success: true };
}

View file

@ -215,17 +215,16 @@ export class PasswordResetController {
this.authService.issueCookie(res, user, req.browserId);
this.internalHooks.onUserUpdate({ user, fields_changed: ['password'] });
this.eventService.emit('user-updated', { user, fieldsChanged: ['password'] });
// if this user used to be an LDAP users
// if this user used to be an LDAP user
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
if (ldapIdentity) {
this.internalHooks.onUserSignup(user, {
user_type: 'email',
was_disabled_ldap_user: true,
this.eventService.emit('user-signed-up', {
user,
userType: 'email',
wasDisabledLdapUser: true,
});
this.eventService.emit('user-signed-up', { user });
}
await this.externalHooks.run('user.password.update', [user.email, passwordHash]);

View file

@ -9,7 +9,7 @@ import {
UserRoleChangePayload,
UserSettingsUpdatePayload,
} from '@/requests';
import type { PublicUser, ITelemetryUserDeletionData } from '@/Interfaces';
import type { PublicUser } from '@/Interfaces';
import { AuthIdentity } from '@db/entities/AuthIdentity';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@ -21,7 +21,6 @@ import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks';
import { validateEntity } from '@/GenericHelpers';
import { ProjectRepository } from '@/databases/repositories/project.repository';
import { Project } from '@/databases/entities/Project';
@ -35,7 +34,6 @@ export class UsersController {
constructor(
private readonly logger: Logger,
private readonly externalHooks: ExternalHooks,
private readonly internalHooks: InternalHooks,
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly userRepository: UserRepository,
@ -183,12 +181,7 @@ export class UsersController {
);
}
const telemetryData: ITelemetryUserDeletionData = {
user_id: req.user.id,
target_user_old_status: userToDelete.isPending ? 'invited' : 'active',
target_user_id: idToDelete,
migration_strategy: transferId ? 'transfer_data' : 'delete_data',
};
let transfereeId;
if (transferId) {
const transfereePersonalProject = await this.projectRepository.findOneBy({ id: transferId });
@ -206,7 +199,7 @@ export class UsersController {
},
});
telemetryData.migration_user_id = transferee.id;
transfereeId = transferee.id;
await this.userService.getManager().transaction(async (trx) => {
await this.workflowService.transferAll(
@ -253,12 +246,14 @@ export class UsersController {
await trx.delete(User, { id: userToDelete.id });
});
this.internalHooks.onUserDeletion({
this.eventService.emit('user-deleted', {
user: req.user,
telemetryData,
publicApi: false,
targetUserOldStatus: userToDelete.isPending ? 'invited' : 'active',
targetUserId: idToDelete,
migrationStrategy: transferId ? 'transfer_data' : 'delete_data',
migrationUserId: transfereeId,
});
this.eventService.emit('user-deleted', { user: req.user });
await this.externalHooks.run('user.deleted', [await this.userService.toPublic(userToDelete)]);
@ -294,11 +289,11 @@ export class UsersController {
await this.userService.update(targetUser.id, { role: payload.newRoleName });
this.internalHooks.onUserRoleChange({
user: req.user,
target_user_id: targetUser.id,
target_user_new_role: ['global', payload.newRoleName].join(' '),
public_api: false,
this.eventService.emit('user-changed-role', {
userId: req.user.id,
targetUserId: targetUser.id,
targetUserNewRole: ['global', payload.newRoleName].join(' '),
publicApi: false,
});
const projects = await this.projectService.getUserOwnedOrAdminProjects(targetUser.id);

View file

@ -242,6 +242,11 @@ describe('LogStreamingEventRelay', () => {
lastName: 'Doe',
role: 'some-role',
},
targetUserOldStatus: 'active',
publicApi: false,
migrationStrategy: 'transfer_data',
targetUserId: '456',
migrationUserId: '789',
};
eventService.emit('user-deleted', event);

View file

@ -2,6 +2,7 @@ import type { AuthenticationMethod, IRun, IWorkflowBase } from 'n8n-workflow';
import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
import type { ProjectRole } from '@/databases/entities/ProjectRelation';
import type { GlobalRole } from '@/databases/entities/User';
import type { AuthProviderType } from '@/databases/entities/AuthIdentity';
export type UserLike = {
id: string;
@ -72,13 +73,26 @@ export type RelayEventMap = {
// #region User
'user-submitted-personalization-survey': {
userId: string;
answers: Record<string, string>;
};
'user-deleted': {
user: UserLike;
publicApi: boolean;
targetUserOldStatus: 'active' | 'invited';
migrationStrategy?: 'transfer_data' | 'delete_data';
targetUserId?: string;
migrationUserId?: string;
};
'user-invited': {
user: UserLike;
targetUserId: string[];
publicApi: boolean;
emailSent: boolean;
inviteeRole: string;
};
'user-reinvited': {
@ -93,6 +107,8 @@ export type RelayEventMap = {
'user-signed-up': {
user: UserLike;
userType: AuthProviderType;
wasDisabledLdapUser: boolean;
};
'user-logged-in': {
@ -106,6 +122,43 @@ export type RelayEventMap = {
reason?: string;
};
'user-changed-role': {
userId: string;
targetUserId: string;
publicApi: boolean;
targetUserNewRole: string;
};
'user-retrieved-user': {
userId: string;
publicApi: boolean;
};
'user-retrieved-all-users': {
userId: string;
publicApi: boolean;
};
'user-retrieved-execution': {
userId: string;
publicApi: boolean;
};
'user-retrieved-all-executions': {
userId: string;
publicApi: boolean;
};
'user-retrieved-workflow': {
userId: string;
publicApi: boolean;
};
'user-retrieved-all-workflows': {
userId: string;
publicApi: boolean;
};
// #endregion
// #region Click

View file

@ -17,6 +17,7 @@ import { ProjectRelationRepository } from '@/databases/repositories/projectRelat
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 {
@ -73,6 +74,19 @@ export class TelemetryEventRelay extends EventRelay {
'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),
});
}
@ -744,4 +758,132 @@ export class TelemetryEventRelay extends EventRelay {
}
// #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
}

View file

@ -151,16 +151,12 @@ export class UserService {
});
}
Container.get(InternalHooks).onUserInvite({
user: owner,
target_user_id: Object.values(toInviteUsers),
public_api: false,
email_sent: result.emailSent,
invitee_role: role, // same role for all invited users
});
this.eventService.emit('user-invited', {
user: owner,
targetUserId: Object.values(toInviteUsers),
publicApi: false,
emailSent: result.emailSent,
inviteeRole: role, // same role for all invited users
});
} catch (e) {
if (e instanceof Error) {