fix(core): Flush instance stopped event immediately (#10238)

This commit is contained in:
Tomi Turtiainen 2024-07-30 14:49:41 +03:00 committed by GitHub
parent a2d08846d0
commit d6770b5fca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 223 additions and 274 deletions

View file

@ -43,13 +43,11 @@ export class InternalHooks {
private readonly projectRelationRepository: ProjectRelationRepository, private readonly projectRelationRepository: ProjectRelationRepository,
private readonly _eventBus: MessageEventBus, // needed until we decouple telemetry private readonly _eventBus: MessageEventBus, // needed until we decouple telemetry
) { ) {
workflowStatisticsService.on( workflowStatisticsService.on('telemetry.onFirstProductionWorkflowSuccess', (metrics) =>
'telemetry.onFirstProductionWorkflowSuccess', this.onFirstProductionWorkflowSuccess(metrics),
async (metrics) => await this.onFirstProductionWorkflowSuccess(metrics),
); );
workflowStatisticsService.on( workflowStatisticsService.on('telemetry.onFirstWorkflowDataLoad', (metrics) =>
'telemetry.onFirstWorkflowDataLoad', this.onFirstWorkflowDataLoad(metrics),
async (metrics) => await this.onFirstWorkflowDataLoad(metrics),
); );
} }
@ -57,35 +55,29 @@ export class InternalHooks {
await this.telemetry.init(); await this.telemetry.init();
} }
async onFrontendSettingsAPI(pushRef?: string): Promise<void> { onFrontendSettingsAPI(pushRef?: string): void {
return await this.telemetry.track('Session started', { session_id: pushRef }); this.telemetry.track('Session started', { session_id: pushRef });
} }
async onPersonalizationSurveySubmitted( onPersonalizationSurveySubmitted(userId: string, answers: Record<string, string>): void {
userId: string,
answers: Record<string, string>,
): Promise<void> {
const camelCaseKeys = Object.keys(answers); const camelCaseKeys = Object.keys(answers);
const personalizationSurveyData = { user_id: userId } as Record<string, string | string[]>; const personalizationSurveyData = { user_id: userId } as Record<string, string | string[]>;
camelCaseKeys.forEach((camelCaseKey) => { camelCaseKeys.forEach((camelCaseKey) => {
personalizationSurveyData[snakeCase(camelCaseKey)] = answers[camelCaseKey]; personalizationSurveyData[snakeCase(camelCaseKey)] = answers[camelCaseKey];
}); });
return await this.telemetry.track( this.telemetry.track('User responded to personalization questions', personalizationSurveyData);
'User responded to personalization questions',
personalizationSurveyData,
);
} }
async onWorkflowCreated( onWorkflowCreated(
user: User, user: User,
workflow: IWorkflowBase, workflow: IWorkflowBase,
project: Project, project: Project,
publicApi: boolean, publicApi: boolean,
): Promise<void> { ): void {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes); const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
void this.telemetry.track('User created workflow', { this.telemetry.track('User created workflow', {
user_id: user.id, user_id: user.id,
workflow_id: workflow.id, workflow_id: workflow.id,
node_graph_string: JSON.stringify(nodeGraph), node_graph_string: JSON.stringify(nodeGraph),
@ -95,8 +87,8 @@ export class InternalHooks {
}); });
} }
async onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): Promise<void> { onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): void {
void this.telemetry.track('User deleted workflow', { this.telemetry.track('User deleted workflow', {
user_id: user.id, user_id: user.id,
workflow_id: workflowId, workflow_id: workflowId,
public_api: publicApi, public_api: publicApi,
@ -136,7 +128,7 @@ export class InternalHooks {
(note) => note.overlapping, (note) => note.overlapping,
).length; ).length;
void this.telemetry.track('User saved workflow', { this.telemetry.track('User saved workflow', {
user_id: user.id, user_id: user.id,
workflow_id: workflow.id, workflow_id: workflow.id,
node_graph_string: JSON.stringify(nodeGraph), node_graph_string: JSON.stringify(nodeGraph),
@ -155,7 +147,7 @@ export class InternalHooks {
workflow: IWorkflowBase, workflow: IWorkflowBase,
runData?: IRun, runData?: IRun,
userId?: string, userId?: string,
): Promise<void> { ) {
if (!workflow.id) { if (!workflow.id) {
return; return;
} }
@ -165,8 +157,6 @@ export class InternalHooks {
return; return;
} }
const promises = [];
const telemetryProperties: IExecutionTrackProperties = { const telemetryProperties: IExecutionTrackProperties = {
workflow_id: workflow.id, workflow_id: workflow.id,
is_manual: false, is_manual: false,
@ -270,7 +260,7 @@ export class InternalHooks {
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode], node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode],
}; };
promises.push(this.telemetry.track('Manual node exec finished', telemetryPayload)); this.telemetry.track('Manual node exec finished', telemetryPayload);
} else { } else {
nodeGraphResult.webhookNodeNames.forEach((name: string) => { nodeGraphResult.webhookNodeNames.forEach((name: string) => {
const execJson = runData.data.resultData.runData[name]?.[0]?.data?.main?.[0]?.[0] const execJson = runData.data.resultData.runData[name]?.[0]?.data?.main?.[0]?.[0]
@ -282,56 +272,52 @@ export class InternalHooks {
} }
}); });
promises.push( this.telemetry.track('Manual workflow exec finished', manualExecEventProperties);
this.telemetry.track('Manual workflow exec finished', manualExecEventProperties),
);
} }
} }
} }
void Promise.all([...promises, this.telemetry.trackWorkflowExecution(telemetryProperties)]); this.telemetry.trackWorkflowExecution(telemetryProperties);
} }
async onWorkflowSharingUpdate(workflowId: string, userId: string, userList: string[]) { onWorkflowSharingUpdate(workflowId: string, userId: string, userList: string[]) {
const properties: ITelemetryTrackProperties = { const properties: ITelemetryTrackProperties = {
workflow_id: workflowId, workflow_id: workflowId,
user_id_sharer: userId, user_id_sharer: userId,
user_id_list: userList, user_id_list: userList,
}; };
return await this.telemetry.track('User updated workflow sharing', properties); this.telemetry.track('User updated workflow sharing', properties);
} }
async onN8nStop(): Promise<void> { async onN8nStop(): Promise<void> {
const timeoutPromise = new Promise<void>((resolve) => { const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => { setTimeout(resolve, 3000);
resolve();
}, 3000);
}); });
return await Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]); return await Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
} }
async onUserDeletion(userDeletionData: { onUserDeletion(userDeletionData: {
user: User; user: User;
telemetryData: ITelemetryUserDeletionData; telemetryData: ITelemetryUserDeletionData;
publicApi: boolean; publicApi: boolean;
}): Promise<void> { }) {
void this.telemetry.track('User deleted user', { this.telemetry.track('User deleted user', {
...userDeletionData.telemetryData, ...userDeletionData.telemetryData,
user_id: userDeletionData.user.id, user_id: userDeletionData.user.id,
public_api: userDeletionData.publicApi, public_api: userDeletionData.publicApi,
}); });
} }
async onUserInvite(userInviteData: { onUserInvite(userInviteData: {
user: User; user: User;
target_user_id: string[]; target_user_id: string[];
public_api: boolean; public_api: boolean;
email_sent: boolean; email_sent: boolean;
invitee_role: string; invitee_role: string;
}): Promise<void> { }) {
void this.telemetry.track('User invited new user', { this.telemetry.track('User invited new user', {
user_id: userInviteData.user.id, user_id: userInviteData.user.id,
target_user_id: userInviteData.target_user_id, target_user_id: userInviteData.target_user_id,
public_api: userInviteData.public_api, public_api: userInviteData.public_api,
@ -340,7 +326,7 @@ export class InternalHooks {
}); });
} }
async onUserRoleChange(userRoleChangeData: { onUserRoleChange(userRoleChangeData: {
user: User; user: User;
target_user_id: string; target_user_id: string;
public_api: boolean; public_api: boolean;
@ -348,74 +334,53 @@ export class InternalHooks {
}) { }) {
const { user, ...rest } = userRoleChangeData; const { user, ...rest } = userRoleChangeData;
void this.telemetry.track('User changed role', { user_id: user.id, ...rest }); this.telemetry.track('User changed role', { user_id: user.id, ...rest });
} }
async onUserRetrievedUser(userRetrievedData: { onUserRetrievedUser(userRetrievedData: { user_id: string; public_api: boolean }) {
user_id: string; this.telemetry.track('User retrieved user', userRetrievedData);
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved user', userRetrievedData);
} }
async onUserRetrievedAllUsers(userRetrievedData: { onUserRetrievedAllUsers(userRetrievedData: { user_id: string; public_api: boolean }) {
user_id: string; this.telemetry.track('User retrieved all users', userRetrievedData);
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved all users', userRetrievedData);
} }
async onUserRetrievedExecution(userRetrievedData: { onUserRetrievedExecution(userRetrievedData: { user_id: string; public_api: boolean }) {
user_id: string; this.telemetry.track('User retrieved execution', userRetrievedData);
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved execution', userRetrievedData);
} }
async onUserRetrievedAllExecutions(userRetrievedData: { onUserRetrievedAllExecutions(userRetrievedData: { user_id: string; public_api: boolean }) {
user_id: string; this.telemetry.track('User retrieved all executions', userRetrievedData);
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved all executions', userRetrievedData);
} }
async onUserRetrievedWorkflow(userRetrievedData: { onUserRetrievedWorkflow(userRetrievedData: { user_id: string; public_api: boolean }) {
user_id: string; this.telemetry.track('User retrieved workflow', userRetrievedData);
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved workflow', userRetrievedData);
} }
async onUserRetrievedAllWorkflows(userRetrievedData: { onUserRetrievedAllWorkflows(userRetrievedData: { user_id: string; public_api: boolean }) {
user_id: string; this.telemetry.track('User retrieved all workflows', userRetrievedData);
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved all workflows', userRetrievedData);
} }
async onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise<void> { onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }) {
void this.telemetry.track('User changed personal settings', { this.telemetry.track('User changed personal settings', {
user_id: userUpdateData.user.id, user_id: userUpdateData.user.id,
fields_changed: userUpdateData.fields_changed, fields_changed: userUpdateData.fields_changed,
}); });
} }
async onUserInviteEmailClick(userInviteClickData: { onUserInviteEmailClick(userInviteClickData: { inviter: User; invitee: User }) {
inviter: User; this.telemetry.track('User clicked invite link from email', {
invitee: User;
}): Promise<void> {
void this.telemetry.track('User clicked invite link from email', {
user_id: userInviteClickData.invitee.id, user_id: userInviteClickData.invitee.id,
}); });
} }
async onUserPasswordResetEmailClick(userPasswordResetData: { user: User }): Promise<void> { onUserPasswordResetEmailClick(userPasswordResetData: { user: User }) {
void this.telemetry.track('User clicked password reset link from email', { this.telemetry.track('User clicked password reset link from email', {
user_id: userPasswordResetData.user.id, user_id: userPasswordResetData.user.id,
}); });
} }
async onUserTransactionalEmail(userTransactionalEmailData: { onUserTransactionalEmail(userTransactionalEmailData: {
user_id: string; user_id: string;
message_type: message_type:
| 'Reset password' | 'Reset password'
@ -424,37 +389,34 @@ export class InternalHooks {
| 'Workflow shared' | 'Workflow shared'
| 'Credentials shared'; | 'Credentials shared';
public_api: boolean; public_api: boolean;
}): Promise<void> { }) {
return await this.telemetry.track( this.telemetry.track('Instance sent transactional email to user', userTransactionalEmailData);
'Instance sent transactional email to user',
userTransactionalEmailData,
);
} }
async onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise<void> { onUserPasswordResetRequestClick(userPasswordResetData: { user: User }) {
void this.telemetry.track('User requested password reset while logged out', { this.telemetry.track('User requested password reset while logged out', {
user_id: userPasswordResetData.user.id, user_id: userPasswordResetData.user.id,
}); });
} }
async onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }): Promise<void> { onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }) {
return await this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData); this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
} }
async onUserSignup( onUserSignup(
user: User, user: User,
userSignupData: { userSignupData: {
user_type: AuthProviderType; user_type: AuthProviderType;
was_disabled_ldap_user: boolean; was_disabled_ldap_user: boolean;
}, },
): Promise<void> { ) {
void this.telemetry.track('User signed up', { this.telemetry.track('User signed up', {
user_id: user.id, user_id: user.id,
...userSignupData, ...userSignupData,
}); });
} }
async onEmailFailed(failedEmailData: { onEmailFailed(failedEmailData: {
user: User; user: User;
message_type: message_type:
| 'Reset password' | 'Reset password'
@ -463,8 +425,8 @@ export class InternalHooks {
| 'Workflow shared' | 'Workflow shared'
| 'Credentials shared'; | 'Credentials shared';
public_api: boolean; public_api: boolean;
}): Promise<void> { }) {
void this.telemetry.track('Instance failed to send transactional email to user', { this.telemetry.track('Instance failed to send transactional email to user', {
user_id: failedEmailData.user.id, user_id: failedEmailData.user.id,
}); });
} }
@ -472,21 +434,18 @@ export class InternalHooks {
/* /*
* Execution Statistics * Execution Statistics
*/ */
async onFirstProductionWorkflowSuccess(data: { onFirstProductionWorkflowSuccess(data: { user_id: string; workflow_id: string }) {
user_id: string; this.telemetry.track('Workflow first prod success', data);
workflow_id: string;
}): Promise<void> {
return await this.telemetry.track('Workflow first prod success', data);
} }
async onFirstWorkflowDataLoad(data: { onFirstWorkflowDataLoad(data: {
user_id: string; user_id: string;
workflow_id: string; workflow_id: string;
node_type: string; node_type: string;
node_id: string; node_id: string;
credential_type?: string; credential_type?: string;
credential_id?: string; credential_id?: string;
}): Promise<void> { }) {
return await this.telemetry.track('Workflow first data fetched', data); this.telemetry.track('Workflow first data fetched', data);
} }
} }

View file

@ -78,7 +78,7 @@ export = {
return res.status(404).json({ message: 'Not Found' }); return res.status(404).json({ message: 'Not Found' });
} }
void Container.get(InternalHooks).onUserRetrievedExecution({ Container.get(InternalHooks).onUserRetrievedExecution({
user_id: req.user.id, user_id: req.user.id,
public_api: true, public_api: true,
}); });
@ -129,7 +129,7 @@ export = {
const count = const count =
await Container.get(ExecutionRepository).getExecutionsCountForPublicApi(filters); await Container.get(ExecutionRepository).getExecutionsCountForPublicApi(filters);
void Container.get(InternalHooks).onUserRetrievedAllExecutions({ Container.get(InternalHooks).onUserRetrievedAllExecutions({
user_id: req.user.id, user_id: req.user.id,
public_api: true, public_api: true,
}); });

View file

@ -33,7 +33,7 @@ export = {
public_api: true, public_api: true,
}; };
void Container.get(InternalHooks).onUserRetrievedUser(telemetryData); Container.get(InternalHooks).onUserRetrievedUser(telemetryData);
return res.json(clean(user, { includeRole })); return res.json(clean(user, { includeRole }));
}, },
@ -56,7 +56,7 @@ export = {
public_api: true, public_api: true,
}; };
void Container.get(InternalHooks).onUserRetrievedAllUsers(telemetryData); Container.get(InternalHooks).onUserRetrievedAllUsers(telemetryData);
return res.json({ return res.json({
data: clean(users, { includeRole }), data: clean(users, { includeRole }),

View file

@ -58,7 +58,7 @@ export = {
); );
await Container.get(ExternalHooks).run('workflow.afterCreate', [createdWorkflow]); await Container.get(ExternalHooks).run('workflow.afterCreate', [createdWorkflow]);
void Container.get(InternalHooks).onWorkflowCreated(req.user, createdWorkflow, project, true); Container.get(InternalHooks).onWorkflowCreated(req.user, createdWorkflow, project, true);
Container.get(EventService).emit('workflow-created', { Container.get(EventService).emit('workflow-created', {
workflow: createdWorkflow, workflow: createdWorkflow,
user: req.user, user: req.user,
@ -101,7 +101,7 @@ export = {
return res.status(404).json({ message: 'Not Found' }); return res.status(404).json({ message: 'Not Found' });
} }
void Container.get(InternalHooks).onUserRetrievedWorkflow({ Container.get(InternalHooks).onUserRetrievedWorkflow({
user_id: req.user.id, user_id: req.user.id,
public_api: true, public_api: true,
}); });
@ -163,7 +163,7 @@ export = {
...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }), ...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }),
}); });
void Container.get(InternalHooks).onUserRetrievedAllWorkflows({ Container.get(InternalHooks).onUserRetrievedAllWorkflows({
user_id: req.user.id, user_id: req.user.id,
public_api: true, public_api: true,
}); });

View file

@ -112,7 +112,7 @@ export class UserManagementMailer {
this.logger.info('Sent workflow shared email successfully', { sharerId: sharer.id }); this.logger.info('Sent workflow shared email successfully', { sharerId: sharer.id });
void Container.get(InternalHooks).onUserTransactionalEmail({ Container.get(InternalHooks).onUserTransactionalEmail({
user_id: sharer.id, user_id: sharer.id,
message_type: 'Workflow shared', message_type: 'Workflow shared',
public_api: false, public_api: false,
@ -120,7 +120,7 @@ export class UserManagementMailer {
return result; return result;
} catch (e) { } catch (e) {
void Container.get(InternalHooks).onEmailFailed({ Container.get(InternalHooks).onEmailFailed({
user: sharer, user: sharer,
message_type: 'Workflow shared', message_type: 'Workflow shared',
public_api: false, public_api: false,
@ -171,7 +171,7 @@ export class UserManagementMailer {
this.logger.info('Sent credentials shared email successfully', { sharerId: sharer.id }); this.logger.info('Sent credentials shared email successfully', { sharerId: sharer.id });
void Container.get(InternalHooks).onUserTransactionalEmail({ Container.get(InternalHooks).onUserTransactionalEmail({
user_id: sharer.id, user_id: sharer.id,
message_type: 'Credentials shared', message_type: 'Credentials shared',
public_api: false, public_api: false,
@ -179,7 +179,7 @@ export class UserManagementMailer {
return result; return result;
} catch (e) { } catch (e) {
void Container.get(InternalHooks).onEmailFailed({ Container.get(InternalHooks).onEmailFailed({
user: sharer, user: sharer,
message_type: 'Credentials shared', message_type: 'Credentials shared',
public_api: false, public_api: false,

View file

@ -51,7 +51,7 @@ export const handleLdapLogin = async (
await updateLdapUserOnLocalDb(identity, ldapAttributesValues); await updateLdapUserOnLocalDb(identity, ldapAttributesValues);
} else { } else {
const user = await createLdapUserOnLocalDb(ldapAttributesValues, ldapId); const user = await createLdapUserOnLocalDb(ldapAttributesValues, ldapId);
void Container.get(InternalHooks).onUserSignup(user, { Container.get(InternalHooks).onUserSignup(user, {
user_type: 'ldap', user_type: 'ldap',
was_disabled_ldap_user: false, was_disabled_ldap_user: false,
}); });

View file

@ -55,7 +55,7 @@ export class ConcurrencyControlService {
this.productionQueue.on('concurrency-check', ({ capacity }) => { this.productionQueue.on('concurrency-check', ({ capacity }) => {
if (this.shouldReport(capacity)) { if (this.shouldReport(capacity)) {
void this.telemetry.track('User hit concurrency limit', { this.telemetry.track('User hit concurrency limit', {
threshold: CLOUD_TEMP_PRODUCTION_LIMIT - capacity, threshold: CLOUD_TEMP_PRODUCTION_LIMIT - capacity,
}); });
} }

View file

@ -179,7 +179,7 @@ export class AuthController {
throw new BadRequestError('Invalid request'); throw new BadRequestError('Invalid request');
} }
void this.internalHooks.onUserInviteEmailClick({ inviter, invitee }); this.internalHooks.onUserInviteEmailClick({ inviter, invitee });
this.eventService.emit('user-invite-email-click', { inviter, invitee }); this.eventService.emit('user-invite-email-click', { inviter, invitee });
const { firstName, lastName } = inviter; const { firstName, lastName } = inviter;

View file

@ -9,7 +9,6 @@ import { Delete, Get, Middleware, Patch, Post, RestController, GlobalScope } fro
import { NodeRequest } from '@/requests'; import { NodeRequest } from '@/requests';
import type { InstalledPackages } from '@db/entities/InstalledPackages'; import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { CommunityPackages } from '@/Interfaces'; import type { CommunityPackages } from '@/Interfaces';
import { InternalHooks } from '@/InternalHooks';
import { Push } from '@/push'; import { Push } from '@/push';
import { CommunityPackagesService } from '@/services/communityPackages.service'; import { CommunityPackagesService } from '@/services/communityPackages.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@ -37,7 +36,6 @@ export function isNpmError(error: unknown): error is { code: number; stdout: str
export class CommunityPackagesController { export class CommunityPackagesController {
constructor( constructor(
private readonly push: Push, private readonly push: Push,
private readonly internalHooks: InternalHooks,
private readonly communityPackagesService: CommunityPackagesService, private readonly communityPackagesService: CommunityPackagesService,
private readonly eventService: EventService, private readonly eventService: EventService,
) {} ) {}

View file

@ -168,7 +168,7 @@ export class InvitationController {
this.authService.issueCookie(res, updatedUser, req.browserId); this.authService.issueCookie(res, updatedUser, req.browserId);
void this.internalHooks.onUserSignup(updatedUser, { this.internalHooks.onUserSignup(updatedUser, {
user_type: 'email', user_type: 'email',
was_disabled_ldap_user: false, was_disabled_ldap_user: false,
}); });

View file

@ -101,7 +101,7 @@ export class MeController {
this.authService.issueCookie(res, user, req.browserId); this.authService.issueCookie(res, user, req.browserId);
const fieldsChanged = Object.keys(payload); const fieldsChanged = Object.keys(payload);
void this.internalHooks.onUserUpdate({ user, fields_changed: fieldsChanged }); this.internalHooks.onUserUpdate({ user, fields_changed: fieldsChanged });
this.eventService.emit('user-updated', { user, fieldsChanged }); this.eventService.emit('user-updated', { user, fieldsChanged });
const publicUser = await this.userService.toPublic(user); const publicUser = await this.userService.toPublic(user);
@ -151,7 +151,7 @@ export class MeController {
this.authService.issueCookie(res, updatedUser, req.browserId); this.authService.issueCookie(res, updatedUser, req.browserId);
void this.internalHooks.onUserUpdate({ user: updatedUser, fields_changed: ['password'] }); this.internalHooks.onUserUpdate({ user: updatedUser, fields_changed: ['password'] });
this.eventService.emit('user-updated', { user: updatedUser, fieldsChanged: ['password'] }); this.eventService.emit('user-updated', { user: updatedUser, fieldsChanged: ['password'] });
await this.externalHooks.run('user.password.update', [updatedUser.email, updatedUser.password]); await this.externalHooks.run('user.password.update', [updatedUser.email, updatedUser.password]);
@ -186,7 +186,7 @@ export class MeController {
this.logger.info('User survey updated successfully', { userId: req.user.id }); this.logger.info('User survey updated successfully', { userId: req.user.id });
void this.internalHooks.onPersonalizationSurveySubmitted(req.user.id, personalizationAnswers); this.internalHooks.onPersonalizationSurveySubmitted(req.user.id, personalizationAnswers);
return { success: true }; return { success: true };
} }

View file

@ -85,7 +85,7 @@ export class OwnerController {
this.authService.issueCookie(res, owner, req.browserId); this.authService.issueCookie(res, owner, req.browserId);
void this.internalHooks.onInstanceOwnerSetup({ user_id: owner.id }); this.internalHooks.onInstanceOwnerSetup({ user_id: owner.id });
return await this.userService.toPublic(owner, { posthog: this.postHog, withScopes: true }); return await this.userService.toPublic(owner, { posthog: this.postHog, withScopes: true });
} }

View file

@ -120,7 +120,7 @@ export class PasswordResetController {
domain: this.urlService.getInstanceBaseUrl(), domain: this.urlService.getInstanceBaseUrl(),
}); });
} catch (error) { } catch (error) {
void this.internalHooks.onEmailFailed({ this.internalHooks.onEmailFailed({
user, user,
message_type: 'Reset password', message_type: 'Reset password',
public_api: false, public_api: false,
@ -132,13 +132,13 @@ export class PasswordResetController {
} }
this.logger.info('Sent password reset email successfully', { userId: user.id, email }); this.logger.info('Sent password reset email successfully', { userId: user.id, email });
void this.internalHooks.onUserTransactionalEmail({ this.internalHooks.onUserTransactionalEmail({
user_id: id, user_id: id,
message_type: 'Reset password', message_type: 'Reset password',
public_api: false, public_api: false,
}); });
void this.internalHooks.onUserPasswordResetRequestClick({ user }); this.internalHooks.onUserPasswordResetRequestClick({ user });
this.eventService.emit('user-password-reset-request-click', { user }); this.eventService.emit('user-password-reset-request-click', { user });
} }
@ -171,7 +171,7 @@ export class PasswordResetController {
} }
this.logger.info('Reset-password token resolved successfully', { userId: user.id }); this.logger.info('Reset-password token resolved successfully', { userId: user.id });
void this.internalHooks.onUserPasswordResetEmailClick({ user }); this.internalHooks.onUserPasswordResetEmailClick({ user });
this.eventService.emit('user-password-reset-email-click', { user }); this.eventService.emit('user-password-reset-email-click', { user });
} }
@ -215,13 +215,13 @@ export class PasswordResetController {
this.authService.issueCookie(res, user, req.browserId); this.authService.issueCookie(res, user, req.browserId);
void this.internalHooks.onUserUpdate({ user, fields_changed: ['password'] }); this.internalHooks.onUserUpdate({ user, fields_changed: ['password'] });
this.eventService.emit('user-updated', { user, fieldsChanged: ['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 users
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap'); const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
if (ldapIdentity) { if (ldapIdentity) {
void this.internalHooks.onUserSignup(user, { this.internalHooks.onUserSignup(user, {
user_type: 'email', user_type: 'email',
was_disabled_ldap_user: true, was_disabled_ldap_user: true,
}); });

View file

@ -253,7 +253,7 @@ export class UsersController {
await trx.delete(User, { id: userToDelete.id }); await trx.delete(User, { id: userToDelete.id });
}); });
void this.internalHooks.onUserDeletion({ this.internalHooks.onUserDeletion({
user: req.user, user: req.user,
telemetryData, telemetryData,
publicApi: false, publicApi: false,
@ -294,7 +294,7 @@ export class UsersController {
await this.userService.update(targetUser.id, { role: payload.newRoleName }); await this.userService.update(targetUser.id, { role: payload.newRoleName });
void this.internalHooks.onUserRoleChange({ this.internalHooks.onUserRoleChange({
user: req.user, user: req.user,
target_user_id: targetUser.id, target_user_id: targetUser.id,
target_user_new_role: ['global', payload.newRoleName].join(' '), target_user_new_role: ['global', payload.newRoleName].join(' '),

View file

@ -244,7 +244,7 @@ export class FrontendService {
} }
getSettings(pushRef?: string): IN8nUISettings { getSettings(pushRef?: string): IN8nUISettings {
void this.internalHooks.onFrontendSettingsAPI(pushRef); this.internalHooks.onFrontendSettingsAPI(pushRef);
const restEndpoint = config.getEnv('endpoints.rest'); const restEndpoint = config.getEnv('endpoints.rest');

View file

@ -144,14 +144,14 @@ export class UserService {
if (result.emailSent) { if (result.emailSent) {
invitedUser.user.emailSent = true; invitedUser.user.emailSent = true;
delete invitedUser.user?.inviteAcceptUrl; delete invitedUser.user?.inviteAcceptUrl;
void Container.get(InternalHooks).onUserTransactionalEmail({ Container.get(InternalHooks).onUserTransactionalEmail({
user_id: id, user_id: id,
message_type: 'New user invite', message_type: 'New user invite',
public_api: false, public_api: false,
}); });
} }
void Container.get(InternalHooks).onUserInvite({ Container.get(InternalHooks).onUserInvite({
user: owner, user: owner,
target_user_id: Object.values(toInviteUsers), target_user_id: Object.values(toInviteUsers),
public_api: false, public_api: false,
@ -164,7 +164,7 @@ export class UserService {
}); });
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
void Container.get(InternalHooks).onEmailFailed({ Container.get(InternalHooks).onEmailFailed({
user: owner, user: owner,
message_type: 'New user invite', message_type: 'New user invite',
public_api: false, public_api: false,

View file

@ -88,13 +88,12 @@ export class Telemetry {
); // every 6 hours ); // every 6 hours
} }
private async pulse(): Promise<unknown> { private async pulse() {
if (!this.rudderStack) { if (!this.rudderStack) {
return; return;
} }
const allPromises = Object.keys(this.executionCountsBuffer) const workflowIdsToReport = Object.keys(this.executionCountsBuffer).filter((workflowId) => {
.filter((workflowId) => {
const data = this.executionCountsBuffer[workflowId]; const data = this.executionCountsBuffer[workflowId];
const sum = const sum =
(data.manual_error?.count ?? 0) + (data.manual_error?.count ?? 0) +
@ -102,16 +101,15 @@ export class Telemetry {
(data.prod_error?.count ?? 0) + (data.prod_error?.count ?? 0) +
(data.prod_success?.count ?? 0); (data.prod_success?.count ?? 0);
return sum > 0; return sum > 0;
}) });
.map(async (workflowId) => {
const promise = this.track('Workflow execution count', { for (const workflowId of workflowIdsToReport) {
this.track('Workflow execution count', {
event_version: '2', event_version: '2',
workflow_id: workflowId, workflow_id: workflowId,
...this.executionCountsBuffer[workflowId], ...this.executionCountsBuffer[workflowId],
}); });
}
return await promise;
});
this.executionCountsBuffer = {}; this.executionCountsBuffer = {};
@ -131,11 +129,11 @@ export class Telemetry {
team_projects: (await Container.get(ProjectRepository).getProjectCounts()).team, team_projects: (await Container.get(ProjectRepository).getProjectCounts()).team,
project_role_count: await Container.get(ProjectRelationRepository).countUsersByRole(), project_role_count: await Container.get(ProjectRelationRepository).countUsersByRole(),
}; };
allPromises.push(this.track('pulse', pulsePacket));
return await Promise.all(allPromises); this.track('pulse', pulsePacket);
} }
async trackWorkflowExecution(properties: IExecutionTrackProperties): Promise<void> { trackWorkflowExecution(properties: IExecutionTrackProperties) {
if (this.rudderStack) { if (this.rudderStack) {
const execTime = new Date(); const execTime = new Date();
const workflowId = properties.workflow_id; const workflowId = properties.workflow_id;
@ -164,44 +162,42 @@ export class Telemetry {
properties.is_manual && properties.is_manual &&
properties.error_node_type?.startsWith('n8n-nodes-base') properties.error_node_type?.startsWith('n8n-nodes-base')
) { ) {
void this.track('Workflow execution errored', properties); this.track('Workflow execution errored', properties);
} }
} }
} }
async trackN8nStop(): Promise<void> { async trackN8nStop(): Promise<void> {
clearInterval(this.pulseIntervalReference); clearInterval(this.pulseIntervalReference);
await this.track('User instance stopped');
void Promise.all([this.postHog.stop(), this.rudderStack?.flush()]); this.track('User instance stopped');
await Promise.all([this.postHog.stop(), this.rudderStack?.flush()]);
}
identify(traits?: { [key: string]: string | number | boolean | object | undefined | null }) {
if (!this.rudderStack) {
return;
} }
async identify(traits?: {
[key: string]: string | number | boolean | object | undefined | null;
}): Promise<void> {
const { instanceId } = this.instanceSettings; const { instanceId } = this.instanceSettings;
return await new Promise<void>((resolve) => {
if (this.rudderStack) { this.rudderStack.identify({
this.rudderStack.identify(
{
userId: instanceId, userId: instanceId,
traits: { ...traits, instanceId }, traits: { ...traits, instanceId },
},
resolve,
);
} else {
resolve();
}
}); });
} }
async track( track(
eventName: string, eventName: string,
properties: ITelemetryTrackProperties = {}, properties: ITelemetryTrackProperties = {},
{ withPostHog } = { withPostHog: false }, // whether to additionally track with PostHog { withPostHog } = { withPostHog: false }, // whether to additionally track with PostHog
): Promise<void> { ) {
if (!this.rudderStack) {
return;
}
const { instanceId } = this.instanceSettings; const { instanceId } = this.instanceSettings;
return await new Promise<void>((resolve) => {
if (this.rudderStack) {
const { user_id } = properties; const { user_id } = properties;
const updatedProperties = { const updatedProperties = {
...properties, ...properties,
@ -219,11 +215,7 @@ export class Telemetry {
this.postHog?.track(payload); this.postHog?.track(payload);
} }
return this.rudderStack.track(payload, resolve); return this.rudderStack.track(payload);
}
return resolve();
});
} }
// test helpers // test helpers

View file

@ -104,7 +104,7 @@ export class TelemetryEventRelay {
} }
private teamProjectUpdated({ userId, role, members, projectId }: Event['team-project-updated']) { private teamProjectUpdated({ userId, role, members, projectId }: Event['team-project-updated']) {
void this.telemetry.track('Project settings updated', { this.telemetry.track('Project settings updated', {
user_id: userId, user_id: userId,
role, role,
// eslint-disable-next-line @typescript-eslint/no-shadow // eslint-disable-next-line @typescript-eslint/no-shadow
@ -120,7 +120,7 @@ export class TelemetryEventRelay {
removalType, removalType,
targetProjectId, targetProjectId,
}: Event['team-project-deleted']) { }: Event['team-project-deleted']) {
void this.telemetry.track('User deleted project', { this.telemetry.track('User deleted project', {
user_id: userId, user_id: userId,
role, role,
project_id: projectId, project_id: projectId,
@ -130,7 +130,7 @@ export class TelemetryEventRelay {
} }
private teamProjectCreated({ userId, role }: Event['team-project-created']) { private teamProjectCreated({ userId, role }: Event['team-project-created']) {
void this.telemetry.track('User created project', { this.telemetry.track('User created project', {
user_id: userId, user_id: userId,
role, role,
}); });
@ -142,7 +142,7 @@ export class TelemetryEventRelay {
repoType, repoType,
connected, connected,
}: Event['source-control-settings-updated']) { }: Event['source-control-settings-updated']) {
void this.telemetry.track('User updated source control settings', { this.telemetry.track('User updated source control settings', {
branch_name: branchName, branch_name: branchName,
read_only_instance: readOnlyInstance, read_only_instance: readOnlyInstance,
repo_type: repoType, repo_type: repoType,
@ -155,7 +155,7 @@ export class TelemetryEventRelay {
workflowConflicts, workflowConflicts,
credConflicts, credConflicts,
}: Event['source-control-user-started-pull-ui']) { }: Event['source-control-user-started-pull-ui']) {
void this.telemetry.track('User started pull via UI', { this.telemetry.track('User started pull via UI', {
workflow_updates: workflowUpdates, workflow_updates: workflowUpdates,
workflow_conflicts: workflowConflicts, workflow_conflicts: workflowConflicts,
cred_conflicts: credConflicts, cred_conflicts: credConflicts,
@ -165,7 +165,7 @@ export class TelemetryEventRelay {
private sourceControlUserFinishedPullUi({ private sourceControlUserFinishedPullUi({
workflowUpdates, workflowUpdates,
}: Event['source-control-user-finished-pull-ui']) { }: Event['source-control-user-finished-pull-ui']) {
void this.telemetry.track('User finished pull via UI', { this.telemetry.track('User finished pull via UI', {
workflow_updates: workflowUpdates, workflow_updates: workflowUpdates,
}); });
} }
@ -178,7 +178,7 @@ export class TelemetryEventRelay {
workflow_updates: workflowUpdates, workflow_updates: workflowUpdates,
forced, forced,
}); });
void this.telemetry.track('User pulled via API', { this.telemetry.track('User pulled via API', {
workflow_updates: workflowUpdates, workflow_updates: workflowUpdates,
forced, forced,
}); });
@ -191,7 +191,7 @@ export class TelemetryEventRelay {
credsEligibleWithConflicts, credsEligibleWithConflicts,
variablesEligible, variablesEligible,
}: Event['source-control-user-started-push-ui']) { }: Event['source-control-user-started-push-ui']) {
void this.telemetry.track('User started push via UI', { this.telemetry.track('User started push via UI', {
workflows_eligible: workflowsEligible, workflows_eligible: workflowsEligible,
workflows_eligible_with_conflicts: workflowsEligibleWithConflicts, workflows_eligible_with_conflicts: workflowsEligibleWithConflicts,
creds_eligible: credsEligible, creds_eligible: credsEligible,
@ -206,7 +206,7 @@ export class TelemetryEventRelay {
credsPushed, credsPushed,
variablesPushed, variablesPushed,
}: Event['source-control-user-finished-push-ui']) { }: Event['source-control-user-finished-push-ui']) {
void this.telemetry.track('User finished push via UI', { this.telemetry.track('User finished push via UI', {
workflows_eligible: workflowsEligible, workflows_eligible: workflowsEligible,
workflows_pushed: workflowsPushed, workflows_pushed: workflowsPushed,
creds_pushed: credsPushed, creds_pushed: credsPushed,
@ -215,13 +215,13 @@ export class TelemetryEventRelay {
} }
private licenseRenewalAttempted({ success }: Event['license-renewal-attempted']) { private licenseRenewalAttempted({ success }: Event['license-renewal-attempted']) {
void this.telemetry.track('Instance attempted to refresh license', { this.telemetry.track('Instance attempted to refresh license', {
success, success,
}); });
} }
private variableCreated() { private variableCreated() {
void this.telemetry.track('User created variable'); this.telemetry.track('User created variable');
} }
private externalSecretsProviderSettingsSaved({ private externalSecretsProviderSettingsSaved({
@ -231,7 +231,7 @@ export class TelemetryEventRelay {
isNew, isNew,
errorMessage, errorMessage,
}: Event['external-secrets-provider-settings-saved']) { }: Event['external-secrets-provider-settings-saved']) {
void this.telemetry.track('User updated external secrets settings', { this.telemetry.track('User updated external secrets settings', {
user_id: userId, user_id: userId,
vault_type: vaultType, vault_type: vaultType,
is_valid: isValid, is_valid: isValid,
@ -241,7 +241,7 @@ export class TelemetryEventRelay {
} }
private publicApiInvoked({ userId, path, method, apiVersion }: Event['public-api-invoked']) { private publicApiInvoked({ userId, path, method, apiVersion }: Event['public-api-invoked']) {
void this.telemetry.track('User invoked API', { this.telemetry.track('User invoked API', {
user_id: userId, user_id: userId,
path, path,
method, method,
@ -252,7 +252,7 @@ export class TelemetryEventRelay {
private publicApiKeyCreated(event: Event['public-api-key-created']) { private publicApiKeyCreated(event: Event['public-api-key-created']) {
const { user, publicApi } = event; const { user, publicApi } = event;
void this.telemetry.track('API key created', { this.telemetry.track('API key created', {
user_id: user.id, user_id: user.id,
public_api: publicApi, public_api: publicApi,
}); });
@ -261,7 +261,7 @@ export class TelemetryEventRelay {
private publicApiKeyDeleted(event: Event['public-api-key-deleted']) { private publicApiKeyDeleted(event: Event['public-api-key-deleted']) {
const { user, publicApi } = event; const { user, publicApi } = event;
void this.telemetry.track('API key deleted', { this.telemetry.track('API key deleted', {
user_id: user.id, user_id: user.id,
public_api: publicApi, public_api: publicApi,
}); });
@ -278,7 +278,7 @@ export class TelemetryEventRelay {
packageAuthorEmail, packageAuthorEmail,
failureReason, failureReason,
}: Event['community-package-installed']) { }: Event['community-package-installed']) {
void this.telemetry.track('cnr package install finished', { this.telemetry.track('cnr package install finished', {
user_id: user.id, user_id: user.id,
input_string: inputString, input_string: inputString,
package_name: packageName, package_name: packageName,
@ -300,7 +300,7 @@ export class TelemetryEventRelay {
packageAuthor, packageAuthor,
packageAuthorEmail, packageAuthorEmail,
}: Event['community-package-updated']) { }: Event['community-package-updated']) {
void this.telemetry.track('cnr package updated', { this.telemetry.track('cnr package updated', {
user_id: user.id, user_id: user.id,
package_name: packageName, package_name: packageName,
package_version_current: packageVersionCurrent, package_version_current: packageVersionCurrent,
@ -319,7 +319,7 @@ export class TelemetryEventRelay {
packageAuthor, packageAuthor,
packageAuthorEmail, packageAuthorEmail,
}: Event['community-package-deleted']) { }: Event['community-package-deleted']) {
void this.telemetry.track('cnr package deleted', { this.telemetry.track('cnr package deleted', {
user_id: user.id, user_id: user.id,
package_name: packageName, package_name: packageName,
package_version: packageVersion, package_version: packageVersion,
@ -336,7 +336,7 @@ export class TelemetryEventRelay {
projectId, projectId,
projectType, projectType,
}: Event['credentials-created']) { }: Event['credentials-created']) {
void this.telemetry.track('User created credentials', { this.telemetry.track('User created credentials', {
user_id: user.id, user_id: user.id,
credential_type: credentialType, credential_type: credentialType,
credential_id: credentialId, credential_id: credentialId,
@ -353,7 +353,7 @@ export class TelemetryEventRelay {
userIdsShareesAdded, userIdsShareesAdded,
shareesRemoved, shareesRemoved,
}: Event['credentials-shared']) { }: Event['credentials-shared']) {
void this.telemetry.track('User updated cred sharing', { this.telemetry.track('User updated cred sharing', {
user_id: user.id, user_id: user.id,
credential_type: credentialType, credential_type: credentialType,
credential_id: credentialId, credential_id: credentialId,
@ -364,7 +364,7 @@ export class TelemetryEventRelay {
} }
private credentialsUpdated({ user, credentialId, credentialType }: Event['credentials-updated']) { private credentialsUpdated({ user, credentialId, credentialType }: Event['credentials-updated']) {
void this.telemetry.track('User updated credentials', { this.telemetry.track('User updated credentials', {
user_id: user.id, user_id: user.id,
credential_type: credentialType, credential_type: credentialType,
credential_id: credentialId, credential_id: credentialId,
@ -372,7 +372,7 @@ export class TelemetryEventRelay {
} }
private credentialsDeleted({ user, credentialId, credentialType }: Event['credentials-deleted']) { private credentialsDeleted({ user, credentialId, credentialType }: Event['credentials-deleted']) {
void this.telemetry.track('User deleted credentials', { this.telemetry.track('User deleted credentials', {
user_id: user.id, user_id: user.id,
credential_type: credentialType, credential_type: credentialType,
credential_id: credentialId, credential_id: credentialId,
@ -385,7 +385,7 @@ export class TelemetryEventRelay {
usersSynced, usersSynced,
error, error,
}: Event['ldap-general-sync-finished']) { }: Event['ldap-general-sync-finished']) {
void this.telemetry.track('Ldap general sync finished', { this.telemetry.track('Ldap general sync finished', {
type, type,
succeeded, succeeded,
users_synced: usersSynced, users_synced: usersSynced,
@ -407,7 +407,7 @@ export class TelemetryEventRelay {
loginLabel, loginLabel,
loginEnabled, loginEnabled,
}: Event['ldap-settings-updated']) { }: Event['ldap-settings-updated']) {
void this.telemetry.track('User updated Ldap settings', { this.telemetry.track('User updated Ldap settings', {
user_id: userId, user_id: userId,
loginIdAttribute, loginIdAttribute,
firstNameAttribute, firstNameAttribute,
@ -424,11 +424,11 @@ export class TelemetryEventRelay {
} }
private ldapLoginSyncFailed({ error }: Event['ldap-login-sync-failed']) { private ldapLoginSyncFailed({ error }: Event['ldap-login-sync-failed']) {
void this.telemetry.track('Ldap login sync failed', { error }); this.telemetry.track('Ldap login sync failed', { error });
} }
private loginFailedDueToLdapDisabled({ userId }: Event['login-failed-due-to-ldap-disabled']) { private loginFailedDueToLdapDisabled({ userId }: Event['login-failed-due-to-ldap-disabled']) {
void this.telemetry.track('User login failed since ldap disabled', { user_ud: userId }); this.telemetry.track('User login failed since ldap disabled', { user_ud: userId });
} }
private async serverStarted() { private async serverStarted() {
@ -489,12 +489,10 @@ export class TelemetryEventRelay {
where: {}, where: {},
}); });
void Promise.all([ this.telemetry.identify(info);
this.telemetry.identify(info),
this.telemetry.track('Instance started', { this.telemetry.track('Instance started', {
...info, ...info,
earliest_workflow_created: firstWorkflow?.createdAt, earliest_workflow_created: firstWorkflow?.createdAt,
}), });
]);
} }
} }

View file

@ -282,7 +282,7 @@ export class WorkflowService {
await this.workflowRepository.delete(workflowId); await this.workflowRepository.delete(workflowId);
await this.binaryDataService.deleteMany(idsForDeletion); await this.binaryDataService.deleteMany(idsForDeletion);
void Container.get(InternalHooks).onWorkflowDeleted(user, workflowId, false); Container.get(InternalHooks).onWorkflowDeleted(user, workflowId, false);
this.eventService.emit('workflow-deleted', { user, workflowId }); this.eventService.emit('workflow-deleted', { user, workflowId });
await this.externalHooks.run('workflow.afterDelete', [workflowId]); await this.externalHooks.run('workflow.afterDelete', [workflowId]);

View file

@ -179,7 +179,7 @@ export class WorkflowsController {
delete savedWorkflowWithMetaData.shared; delete savedWorkflowWithMetaData.shared;
await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]); await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
void this.internalHooks.onWorkflowCreated(req.user, newWorkflow, project!, false); this.internalHooks.onWorkflowCreated(req.user, newWorkflow, project!, false);
this.eventService.emit('workflow-created', { user: req.user, workflow: newWorkflow }); this.eventService.emit('workflow-created', { user: req.user, workflow: newWorkflow });
const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id); const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id);
@ -454,7 +454,7 @@ export class WorkflowsController {
newShareeIds = toShare; newShareeIds = toShare;
}); });
void this.internalHooks.onWorkflowSharingUpdate(workflowId, req.user.id, shareWithIds); this.internalHooks.onWorkflowSharingUpdate(workflowId, req.user.id, shareWithIds);
const projectsRelations = await this.projectRelationRepository.findBy({ const projectsRelations = await this.projectRelationRepository.findBy({
projectId: In(newShareeIds), projectId: In(newShareeIds),

View file

@ -14,11 +14,12 @@ describe('Telemetry', () => {
let startPulseSpy: jest.SpyInstance; let startPulseSpy: jest.SpyInstance;
const spyTrack = jest.spyOn(Telemetry.prototype, 'track').mockName('track'); const spyTrack = jest.spyOn(Telemetry.prototype, 'track').mockName('track');
const mockRudderStack: Pick<RudderStack, 'flush' | 'identify' | 'track'> = { const mockRudderStack = mock<RudderStack>();
flush: (resolve) => resolve?.(), mockRudderStack.track.mockImplementation(function (_, cb) {
identify: (data, resolve) => resolve?.(), cb?.();
track: (data, resolve) => resolve?.(),
}; return this;
});
let telemetry: Telemetry; let telemetry: Telemetry;
const instanceId = 'Telemetry unit test'; const instanceId = 'Telemetry unit test';
@ -26,9 +27,9 @@ describe('Telemetry', () => {
const instanceSettings = mockInstance(InstanceSettings, { instanceId }); const instanceSettings = mockInstance(InstanceSettings, { instanceId });
beforeAll(() => { beforeAll(() => {
startPulseSpy = jest // @ts-expect-error Spying on private method
.spyOn(Telemetry.prototype as any, 'startPulse') startPulseSpy = jest.spyOn(Telemetry.prototype, 'startPulse').mockImplementation(() => {});
.mockImplementation(() => {});
jest.useFakeTimers(); jest.useFakeTimers();
jest.setSystemTime(testDateTime); jest.setSystemTime(testDateTime);
config.set('diagnostics.enabled', true); config.set('diagnostics.enabled', true);
@ -49,7 +50,8 @@ describe('Telemetry', () => {
await postHog.init(); await postHog.init();
telemetry = new Telemetry(mock(), postHog, mock(), instanceSettings, mock()); telemetry = new Telemetry(mock(), postHog, mock(), instanceSettings, mock());
(telemetry as any).rudderStack = mockRudderStack; // @ts-expect-error Assigning to private property
telemetry.rudderStack = mockRudderStack;
}); });
afterEach(async () => { afterEach(async () => {
@ -79,30 +81,30 @@ describe('Telemetry', () => {
payload.is_manual = true; payload.is_manual = true;
payload.success = true; payload.success = true;
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00'); const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00'); fakeJestSystemTime('2022-01-01 12:30:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
payload.is_manual = false; payload.is_manual = false;
payload.success = true; payload.success = true;
const execTime2 = fakeJestSystemTime('2022-01-01 13:00:00'); const execTime2 = fakeJestSystemTime('2022-01-01 13:00:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00'); fakeJestSystemTime('2022-01-01 12:30:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
payload.is_manual = true; payload.is_manual = true;
payload.success = false; payload.success = false;
const execTime3 = fakeJestSystemTime('2022-01-01 14:00:00'); const execTime3 = fakeJestSystemTime('2022-01-01 14:00:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00'); fakeJestSystemTime('2022-01-01 12:30:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
payload.is_manual = false; payload.is_manual = false;
payload.success = false; payload.success = false;
const execTime4 = fakeJestSystemTime('2022-01-01 15:00:00'); const execTime4 = fakeJestSystemTime('2022-01-01 15:00:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00'); fakeJestSystemTime('2022-01-01 12:30:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
expect(spyTrack).toHaveBeenCalledTimes(0); expect(spyTrack).toHaveBeenCalledTimes(0);
@ -127,9 +129,9 @@ describe('Telemetry', () => {
}; };
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00'); const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00'); fakeJestSystemTime('2022-01-01 12:30:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
let execBuffer = telemetry.getCountsBuffer(); let execBuffer = telemetry.getCountsBuffer();
@ -140,9 +142,9 @@ describe('Telemetry', () => {
payload.error_node_type = 'n8n-nodes-base.node-type'; payload.error_node_type = 'n8n-nodes-base.node-type';
fakeJestSystemTime('2022-01-01 13:00:00'); fakeJestSystemTime('2022-01-01 13:00:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
fakeJestSystemTime('2022-01-01 12:30:00'); fakeJestSystemTime('2022-01-01 12:30:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
execBuffer = telemetry.getCountsBuffer(); execBuffer = telemetry.getCountsBuffer();
@ -163,7 +165,7 @@ describe('Telemetry', () => {
// successful execution // successful execution
const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00'); const execTime1 = fakeJestSystemTime('2022-01-01 12:00:00');
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
expect(spyTrack).toHaveBeenCalledTimes(0); expect(spyTrack).toHaveBeenCalledTimes(0);
@ -179,7 +181,7 @@ describe('Telemetry', () => {
payload.error_node_type = 'n8n-nodes-base.merge'; payload.error_node_type = 'n8n-nodes-base.merge';
payload.workflow_id = '2'; payload.workflow_id = '2';
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
expect(spyTrack).toHaveBeenCalledTimes(0); expect(spyTrack).toHaveBeenCalledTimes(0);
@ -198,12 +200,12 @@ describe('Telemetry', () => {
payload.error_node_type = 'n8n-nodes-base.merge'; payload.error_node_type = 'n8n-nodes-base.merge';
payload.workflow_id = '2'; payload.workflow_id = '2';
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
payload.error_node_type = 'n8n-nodes-base.merge'; payload.error_node_type = 'n8n-nodes-base.merge';
payload.workflow_id = '1'; payload.workflow_id = '1';
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
expect(spyTrack).toHaveBeenCalledTimes(0); expect(spyTrack).toHaveBeenCalledTimes(0);
execBuffer = telemetry.getCountsBuffer(); execBuffer = telemetry.getCountsBuffer();
@ -225,7 +227,7 @@ describe('Telemetry', () => {
const execTime2 = fakeJestSystemTime('2022-01-01 12:00:00'); const execTime2 = fakeJestSystemTime('2022-01-01 12:00:00');
payload.error_node_type = 'custom-package.custom-node'; payload.error_node_type = 'custom-package.custom-node';
payload.success = false; payload.success = false;
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
expect(spyTrack).toHaveBeenCalledTimes(0); expect(spyTrack).toHaveBeenCalledTimes(0);
@ -249,7 +251,7 @@ describe('Telemetry', () => {
payload.success = false; payload.success = false;
payload.error_node_type = 'n8n-nodes-base.merge'; payload.error_node_type = 'n8n-nodes-base.merge';
payload.is_manual = true; payload.is_manual = true;
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
expect(spyTrack).toHaveBeenCalledTimes(1); expect(spyTrack).toHaveBeenCalledTimes(1);
@ -327,27 +329,27 @@ describe('Telemetry', () => {
error_node_type: 'custom-nodes-base.node-type', error_node_type: 'custom-nodes-base.node-type',
}; };
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
payload.is_manual = false; payload.is_manual = false;
payload.success = true; payload.success = true;
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
payload.is_manual = true; payload.is_manual = true;
payload.success = false; payload.success = false;
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
payload.is_manual = false; payload.is_manual = false;
payload.success = false; payload.success = false;
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
payload.workflow_id = '2'; payload.workflow_id = '2';
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
await telemetry.trackWorkflowExecution(payload); telemetry.trackWorkflowExecution(payload);
expect(spyTrack).toHaveBeenCalledTimes(0); expect(spyTrack).toHaveBeenCalledTimes(0);
expect(pulseSpy).toBeCalledTimes(0); expect(pulseSpy).toBeCalledTimes(0);