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 _eventBus: MessageEventBus, // needed until we decouple telemetry
) {
workflowStatisticsService.on(
'telemetry.onFirstProductionWorkflowSuccess',
async (metrics) => await this.onFirstProductionWorkflowSuccess(metrics),
workflowStatisticsService.on('telemetry.onFirstProductionWorkflowSuccess', (metrics) =>
this.onFirstProductionWorkflowSuccess(metrics),
);
workflowStatisticsService.on(
'telemetry.onFirstWorkflowDataLoad',
async (metrics) => await this.onFirstWorkflowDataLoad(metrics),
workflowStatisticsService.on('telemetry.onFirstWorkflowDataLoad', (metrics) =>
this.onFirstWorkflowDataLoad(metrics),
);
}
@ -57,35 +55,29 @@ export class InternalHooks {
await this.telemetry.init();
}
async onFrontendSettingsAPI(pushRef?: string): Promise<void> {
return await this.telemetry.track('Session started', { session_id: pushRef });
onFrontendSettingsAPI(pushRef?: string): void {
this.telemetry.track('Session started', { session_id: pushRef });
}
async onPersonalizationSurveySubmitted(
userId: string,
answers: Record<string, string>,
): Promise<void> {
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];
});
return await this.telemetry.track(
'User responded to personalization questions',
personalizationSurveyData,
);
this.telemetry.track('User responded to personalization questions', personalizationSurveyData);
}
async onWorkflowCreated(
onWorkflowCreated(
user: User,
workflow: IWorkflowBase,
project: Project,
publicApi: boolean,
): Promise<void> {
): void {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
void this.telemetry.track('User created workflow', {
this.telemetry.track('User created workflow', {
user_id: user.id,
workflow_id: workflow.id,
node_graph_string: JSON.stringify(nodeGraph),
@ -95,8 +87,8 @@ export class InternalHooks {
});
}
async onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): Promise<void> {
void this.telemetry.track('User deleted workflow', {
onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): void {
this.telemetry.track('User deleted workflow', {
user_id: user.id,
workflow_id: workflowId,
public_api: publicApi,
@ -136,7 +128,7 @@ export class InternalHooks {
(note) => note.overlapping,
).length;
void this.telemetry.track('User saved workflow', {
this.telemetry.track('User saved workflow', {
user_id: user.id,
workflow_id: workflow.id,
node_graph_string: JSON.stringify(nodeGraph),
@ -155,7 +147,7 @@ export class InternalHooks {
workflow: IWorkflowBase,
runData?: IRun,
userId?: string,
): Promise<void> {
) {
if (!workflow.id) {
return;
}
@ -165,8 +157,6 @@ export class InternalHooks {
return;
}
const promises = [];
const telemetryProperties: IExecutionTrackProperties = {
workflow_id: workflow.id,
is_manual: false,
@ -270,7 +260,7 @@ export class InternalHooks {
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 {
nodeGraphResult.webhookNodeNames.forEach((name: string) => {
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 = {
workflow_id: workflowId,
user_id_sharer: userId,
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> {
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 3000);
setTimeout(resolve, 3000);
});
return await Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
}
async onUserDeletion(userDeletionData: {
onUserDeletion(userDeletionData: {
user: User;
telemetryData: ITelemetryUserDeletionData;
publicApi: boolean;
}): Promise<void> {
void this.telemetry.track('User deleted user', {
}) {
this.telemetry.track('User deleted user', {
...userDeletionData.telemetryData,
user_id: userDeletionData.user.id,
public_api: userDeletionData.publicApi,
});
}
async onUserInvite(userInviteData: {
onUserInvite(userInviteData: {
user: User;
target_user_id: string[];
public_api: boolean;
email_sent: boolean;
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,
target_user_id: userInviteData.target_user_id,
public_api: userInviteData.public_api,
@ -340,7 +326,7 @@ export class InternalHooks {
});
}
async onUserRoleChange(userRoleChangeData: {
onUserRoleChange(userRoleChangeData: {
user: User;
target_user_id: string;
public_api: boolean;
@ -348,74 +334,53 @@ export class InternalHooks {
}) {
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: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved user', userRetrievedData);
onUserRetrievedUser(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved user', userRetrievedData);
}
async onUserRetrievedAllUsers(userRetrievedData: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved all users', userRetrievedData);
onUserRetrievedAllUsers(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved all users', userRetrievedData);
}
async onUserRetrievedExecution(userRetrievedData: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved execution', userRetrievedData);
onUserRetrievedExecution(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved execution', userRetrievedData);
}
async onUserRetrievedAllExecutions(userRetrievedData: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved all executions', userRetrievedData);
onUserRetrievedAllExecutions(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved all executions', userRetrievedData);
}
async onUserRetrievedWorkflow(userRetrievedData: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved workflow', userRetrievedData);
onUserRetrievedWorkflow(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved workflow', userRetrievedData);
}
async onUserRetrievedAllWorkflows(userRetrievedData: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track('User retrieved all workflows', userRetrievedData);
onUserRetrievedAllWorkflows(userRetrievedData: { user_id: string; public_api: boolean }) {
this.telemetry.track('User retrieved all workflows', userRetrievedData);
}
async onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise<void> {
void this.telemetry.track('User changed personal settings', {
onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }) {
this.telemetry.track('User changed personal settings', {
user_id: userUpdateData.user.id,
fields_changed: userUpdateData.fields_changed,
});
}
async onUserInviteEmailClick(userInviteClickData: {
inviter: User;
invitee: User;
}): Promise<void> {
void this.telemetry.track('User clicked invite link from email', {
onUserInviteEmailClick(userInviteClickData: { inviter: User; invitee: User }) {
this.telemetry.track('User clicked invite link from email', {
user_id: userInviteClickData.invitee.id,
});
}
async onUserPasswordResetEmailClick(userPasswordResetData: { user: User }): Promise<void> {
void this.telemetry.track('User clicked password reset link from email', {
onUserPasswordResetEmailClick(userPasswordResetData: { user: User }) {
this.telemetry.track('User clicked password reset link from email', {
user_id: userPasswordResetData.user.id,
});
}
async onUserTransactionalEmail(userTransactionalEmailData: {
onUserTransactionalEmail(userTransactionalEmailData: {
user_id: string;
message_type:
| 'Reset password'
@ -424,37 +389,34 @@ export class InternalHooks {
| 'Workflow shared'
| 'Credentials shared';
public_api: boolean;
}): Promise<void> {
return await this.telemetry.track(
'Instance sent transactional email to user',
userTransactionalEmailData,
);
}) {
this.telemetry.track('Instance sent transactional email to user', userTransactionalEmailData);
}
async onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise<void> {
void this.telemetry.track('User requested password reset while logged out', {
onUserPasswordResetRequestClick(userPasswordResetData: { user: User }) {
this.telemetry.track('User requested password reset while logged out', {
user_id: userPasswordResetData.user.id,
});
}
async onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }): Promise<void> {
return await this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }) {
this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
}
async onUserSignup(
onUserSignup(
user: User,
userSignupData: {
user_type: AuthProviderType;
was_disabled_ldap_user: boolean;
},
): Promise<void> {
void this.telemetry.track('User signed up', {
) {
this.telemetry.track('User signed up', {
user_id: user.id,
...userSignupData,
});
}
async onEmailFailed(failedEmailData: {
onEmailFailed(failedEmailData: {
user: User;
message_type:
| 'Reset password'
@ -463,8 +425,8 @@ export class InternalHooks {
| 'Workflow shared'
| 'Credentials shared';
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,
});
}
@ -472,21 +434,18 @@ export class InternalHooks {
/*
* Execution Statistics
*/
async onFirstProductionWorkflowSuccess(data: {
user_id: string;
workflow_id: string;
}): Promise<void> {
return await this.telemetry.track('Workflow first prod success', data);
onFirstProductionWorkflowSuccess(data: { user_id: string; workflow_id: string }) {
this.telemetry.track('Workflow first prod success', data);
}
async onFirstWorkflowDataLoad(data: {
onFirstWorkflowDataLoad(data: {
user_id: string;
workflow_id: string;
node_type: string;
node_id: string;
credential_type?: 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' });
}
void Container.get(InternalHooks).onUserRetrievedExecution({
Container.get(InternalHooks).onUserRetrievedExecution({
user_id: req.user.id,
public_api: true,
});
@ -129,7 +129,7 @@ export = {
const count =
await Container.get(ExecutionRepository).getExecutionsCountForPublicApi(filters);
void Container.get(InternalHooks).onUserRetrievedAllExecutions({
Container.get(InternalHooks).onUserRetrievedAllExecutions({
user_id: req.user.id,
public_api: true,
});

View file

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

View file

@ -58,7 +58,7 @@ export = {
);
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', {
workflow: createdWorkflow,
user: req.user,
@ -101,7 +101,7 @@ export = {
return res.status(404).json({ message: 'Not Found' });
}
void Container.get(InternalHooks).onUserRetrievedWorkflow({
Container.get(InternalHooks).onUserRetrievedWorkflow({
user_id: req.user.id,
public_api: true,
});
@ -163,7 +163,7 @@ export = {
...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }),
});
void Container.get(InternalHooks).onUserRetrievedAllWorkflows({
Container.get(InternalHooks).onUserRetrievedAllWorkflows({
user_id: req.user.id,
public_api: true,
});

View file

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

View file

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

View file

@ -55,7 +55,7 @@ export class ConcurrencyControlService {
this.productionQueue.on('concurrency-check', ({ 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,
});
}

View file

@ -179,7 +179,7 @@ export class AuthController {
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 });
const { firstName, lastName } = inviter;

View file

@ -9,7 +9,6 @@ import { Delete, Get, Middleware, Patch, Post, RestController, GlobalScope } fro
import { NodeRequest } from '@/requests';
import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { CommunityPackages } from '@/Interfaces';
import { InternalHooks } from '@/InternalHooks';
import { Push } from '@/push';
import { CommunityPackagesService } from '@/services/communityPackages.service';
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 {
constructor(
private readonly push: Push,
private readonly internalHooks: InternalHooks,
private readonly communityPackagesService: CommunityPackagesService,
private readonly eventService: EventService,
) {}

View file

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

View file

@ -101,7 +101,7 @@ export class MeController {
this.authService.issueCookie(res, user, req.browserId);
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 });
const publicUser = await this.userService.toPublic(user);
@ -151,7 +151,7 @@ export class MeController {
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'] });
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 });
void this.internalHooks.onPersonalizationSurveySubmitted(req.user.id, personalizationAnswers);
this.internalHooks.onPersonalizationSurveySubmitted(req.user.id, personalizationAnswers);
return { success: true };
}

View file

@ -85,7 +85,7 @@ export class OwnerController {
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 });
}

View file

@ -120,7 +120,7 @@ export class PasswordResetController {
domain: this.urlService.getInstanceBaseUrl(),
});
} catch (error) {
void this.internalHooks.onEmailFailed({
this.internalHooks.onEmailFailed({
user,
message_type: 'Reset password',
public_api: false,
@ -132,13 +132,13 @@ export class PasswordResetController {
}
this.logger.info('Sent password reset email successfully', { userId: user.id, email });
void this.internalHooks.onUserTransactionalEmail({
this.internalHooks.onUserTransactionalEmail({
user_id: id,
message_type: 'Reset password',
public_api: false,
});
void this.internalHooks.onUserPasswordResetRequestClick({ user });
this.internalHooks.onUserPasswordResetRequestClick({ 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 });
void this.internalHooks.onUserPasswordResetEmailClick({ user });
this.internalHooks.onUserPasswordResetEmailClick({ user });
this.eventService.emit('user-password-reset-email-click', { user });
}
@ -215,13 +215,13 @@ export class PasswordResetController {
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'] });
// if this user used to be an LDAP users
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
if (ldapIdentity) {
void this.internalHooks.onUserSignup(user, {
this.internalHooks.onUserSignup(user, {
user_type: 'email',
was_disabled_ldap_user: true,
});

View file

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

View file

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

View file

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

View file

@ -88,30 +88,28 @@ export class Telemetry {
); // every 6 hours
}
private async pulse(): Promise<unknown> {
private async pulse() {
if (!this.rudderStack) {
return;
}
const allPromises = Object.keys(this.executionCountsBuffer)
.filter((workflowId) => {
const data = this.executionCountsBuffer[workflowId];
const sum =
(data.manual_error?.count ?? 0) +
(data.manual_success?.count ?? 0) +
(data.prod_error?.count ?? 0) +
(data.prod_success?.count ?? 0);
return sum > 0;
})
.map(async (workflowId) => {
const promise = this.track('Workflow execution count', {
event_version: '2',
workflow_id: workflowId,
...this.executionCountsBuffer[workflowId],
});
const workflowIdsToReport = Object.keys(this.executionCountsBuffer).filter((workflowId) => {
const data = this.executionCountsBuffer[workflowId];
const sum =
(data.manual_error?.count ?? 0) +
(data.manual_success?.count ?? 0) +
(data.prod_error?.count ?? 0) +
(data.prod_success?.count ?? 0);
return sum > 0;
});
return await promise;
for (const workflowId of workflowIdsToReport) {
this.track('Workflow execution count', {
event_version: '2',
workflow_id: workflowId,
...this.executionCountsBuffer[workflowId],
});
}
this.executionCountsBuffer = {};
@ -131,11 +129,11 @@ export class Telemetry {
team_projects: (await Container.get(ProjectRepository).getProjectCounts()).team,
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) {
const execTime = new Date();
const workflowId = properties.workflow_id;
@ -164,66 +162,60 @@ export class Telemetry {
properties.is_manual &&
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> {
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()]);
}
async identify(traits?: {
[key: string]: string | number | boolean | object | undefined | null;
}): Promise<void> {
identify(traits?: { [key: string]: string | number | boolean | object | undefined | null }) {
if (!this.rudderStack) {
return;
}
const { instanceId } = this.instanceSettings;
return await new Promise<void>((resolve) => {
if (this.rudderStack) {
this.rudderStack.identify(
{
userId: instanceId,
traits: { ...traits, instanceId },
},
resolve,
);
} else {
resolve();
}
this.rudderStack.identify({
userId: instanceId,
traits: { ...traits, instanceId },
});
}
async track(
track(
eventName: string,
properties: ITelemetryTrackProperties = {},
{ withPostHog } = { withPostHog: false }, // whether to additionally track with PostHog
): Promise<void> {
) {
if (!this.rudderStack) {
return;
}
const { instanceId } = this.instanceSettings;
return await new Promise<void>((resolve) => {
if (this.rudderStack) {
const { user_id } = properties;
const updatedProperties = {
...properties,
instance_id: instanceId,
version_cli: N8N_VERSION,
};
const { user_id } = properties;
const updatedProperties = {
...properties,
instance_id: instanceId,
version_cli: N8N_VERSION,
};
const payload = {
userId: `${instanceId}${user_id ? `#${user_id}` : ''}`,
event: eventName,
properties: updatedProperties,
};
const payload = {
userId: `${instanceId}${user_id ? `#${user_id}` : ''}`,
event: eventName,
properties: updatedProperties,
};
if (withPostHog) {
this.postHog?.track(payload);
}
if (withPostHog) {
this.postHog?.track(payload);
}
return this.rudderStack.track(payload, resolve);
}
return resolve();
});
return this.rudderStack.track(payload);
}
// test helpers

View file

@ -104,7 +104,7 @@ export class TelemetryEventRelay {
}
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,
role,
// eslint-disable-next-line @typescript-eslint/no-shadow
@ -120,7 +120,7 @@ export class TelemetryEventRelay {
removalType,
targetProjectId,
}: Event['team-project-deleted']) {
void this.telemetry.track('User deleted project', {
this.telemetry.track('User deleted project', {
user_id: userId,
role,
project_id: projectId,
@ -130,7 +130,7 @@ export class TelemetryEventRelay {
}
private teamProjectCreated({ userId, role }: Event['team-project-created']) {
void this.telemetry.track('User created project', {
this.telemetry.track('User created project', {
user_id: userId,
role,
});
@ -142,7 +142,7 @@ export class TelemetryEventRelay {
repoType,
connected,
}: 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,
read_only_instance: readOnlyInstance,
repo_type: repoType,
@ -155,7 +155,7 @@ export class TelemetryEventRelay {
workflowConflicts,
credConflicts,
}: 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_conflicts: workflowConflicts,
cred_conflicts: credConflicts,
@ -165,7 +165,7 @@ export class TelemetryEventRelay {
private sourceControlUserFinishedPullUi({
workflowUpdates,
}: 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,
});
}
@ -178,7 +178,7 @@ export class TelemetryEventRelay {
workflow_updates: workflowUpdates,
forced,
});
void this.telemetry.track('User pulled via API', {
this.telemetry.track('User pulled via API', {
workflow_updates: workflowUpdates,
forced,
});
@ -191,7 +191,7 @@ export class TelemetryEventRelay {
credsEligibleWithConflicts,
variablesEligible,
}: 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_with_conflicts: workflowsEligibleWithConflicts,
creds_eligible: credsEligible,
@ -206,7 +206,7 @@ export class TelemetryEventRelay {
credsPushed,
variablesPushed,
}: 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_pushed: workflowsPushed,
creds_pushed: credsPushed,
@ -215,13 +215,13 @@ export class TelemetryEventRelay {
}
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,
});
}
private variableCreated() {
void this.telemetry.track('User created variable');
this.telemetry.track('User created variable');
}
private externalSecretsProviderSettingsSaved({
@ -231,7 +231,7 @@ export class TelemetryEventRelay {
isNew,
errorMessage,
}: 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,
vault_type: vaultType,
is_valid: isValid,
@ -241,7 +241,7 @@ export class TelemetryEventRelay {
}
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,
path,
method,
@ -252,7 +252,7 @@ export class TelemetryEventRelay {
private publicApiKeyCreated(event: Event['public-api-key-created']) {
const { user, publicApi } = event;
void this.telemetry.track('API key created', {
this.telemetry.track('API key created', {
user_id: user.id,
public_api: publicApi,
});
@ -261,7 +261,7 @@ export class TelemetryEventRelay {
private publicApiKeyDeleted(event: Event['public-api-key-deleted']) {
const { user, publicApi } = event;
void this.telemetry.track('API key deleted', {
this.telemetry.track('API key deleted', {
user_id: user.id,
public_api: publicApi,
});
@ -278,7 +278,7 @@ export class TelemetryEventRelay {
packageAuthorEmail,
failureReason,
}: Event['community-package-installed']) {
void this.telemetry.track('cnr package install finished', {
this.telemetry.track('cnr package install finished', {
user_id: user.id,
input_string: inputString,
package_name: packageName,
@ -300,7 +300,7 @@ export class TelemetryEventRelay {
packageAuthor,
packageAuthorEmail,
}: Event['community-package-updated']) {
void this.telemetry.track('cnr package updated', {
this.telemetry.track('cnr package updated', {
user_id: user.id,
package_name: packageName,
package_version_current: packageVersionCurrent,
@ -319,7 +319,7 @@ export class TelemetryEventRelay {
packageAuthor,
packageAuthorEmail,
}: Event['community-package-deleted']) {
void this.telemetry.track('cnr package deleted', {
this.telemetry.track('cnr package deleted', {
user_id: user.id,
package_name: packageName,
package_version: packageVersion,
@ -336,7 +336,7 @@ export class TelemetryEventRelay {
projectId,
projectType,
}: Event['credentials-created']) {
void this.telemetry.track('User created credentials', {
this.telemetry.track('User created credentials', {
user_id: user.id,
credential_type: credentialType,
credential_id: credentialId,
@ -353,7 +353,7 @@ export class TelemetryEventRelay {
userIdsShareesAdded,
shareesRemoved,
}: Event['credentials-shared']) {
void this.telemetry.track('User updated cred sharing', {
this.telemetry.track('User updated cred sharing', {
user_id: user.id,
credential_type: credentialType,
credential_id: credentialId,
@ -364,7 +364,7 @@ export class TelemetryEventRelay {
}
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,
credential_type: credentialType,
credential_id: credentialId,
@ -372,7 +372,7 @@ export class TelemetryEventRelay {
}
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,
credential_type: credentialType,
credential_id: credentialId,
@ -385,7 +385,7 @@ export class TelemetryEventRelay {
usersSynced,
error,
}: Event['ldap-general-sync-finished']) {
void this.telemetry.track('Ldap general sync finished', {
this.telemetry.track('Ldap general sync finished', {
type,
succeeded,
users_synced: usersSynced,
@ -407,7 +407,7 @@ export class TelemetryEventRelay {
loginLabel,
loginEnabled,
}: Event['ldap-settings-updated']) {
void this.telemetry.track('User updated Ldap settings', {
this.telemetry.track('User updated Ldap settings', {
user_id: userId,
loginIdAttribute,
firstNameAttribute,
@ -424,11 +424,11 @@ export class TelemetryEventRelay {
}
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']) {
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() {
@ -489,12 +489,10 @@ export class TelemetryEventRelay {
where: {},
});
void Promise.all([
this.telemetry.identify(info),
this.telemetry.track('Instance started', {
...info,
earliest_workflow_created: firstWorkflow?.createdAt,
}),
]);
this.telemetry.identify(info);
this.telemetry.track('Instance started', {
...info,
earliest_workflow_created: firstWorkflow?.createdAt,
});
}
}

View file

@ -282,7 +282,7 @@ export class WorkflowService {
await this.workflowRepository.delete(workflowId);
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 });
await this.externalHooks.run('workflow.afterDelete', [workflowId]);

View file

@ -179,7 +179,7 @@ export class WorkflowsController {
delete savedWorkflowWithMetaData.shared;
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 });
const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id);
@ -454,7 +454,7 @@ export class WorkflowsController {
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({
projectId: In(newShareeIds),

View file

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