From fdff51fb51a012c928d62bf253e18de538e23843 Mon Sep 17 00:00:00 2001 From: Daniel Gak Anagrov Date: Thu, 15 May 2025 07:20:50 +0200 Subject: [PATCH] feat(workspaces): mixpanel update more attributes (#4713) * mixpanel recieve more workspace attributes --- .../modules/workspaces/domain/operations.ts | 5 + .../workspaces/events/eventListener.ts | 103 ++++++++++++++---- .../workspaces/repositories/workspaces.ts | 19 +++- 3 files changed, 106 insertions(+), 21 deletions(-) diff --git a/packages/server/modules/workspaces/domain/operations.ts b/packages/server/modules/workspaces/domain/operations.ts index 36945e3a2..5fa1befda 100644 --- a/packages/server/modules/workspaces/domain/operations.ts +++ b/packages/server/modules/workspaces/domain/operations.ts @@ -368,6 +368,11 @@ export type CountWorkspaceRoleWithOptionalProjectRole = (args: { skipUserIds?: string[] }) => Promise +export type GetWorkspaceSeatsCount = (args: { + workspaceId: string + type?: WorkspaceSeatType +}) => Promise + export type GetUserIdsWithRoleInWorkspace = ( args: { workspaceId: string diff --git a/packages/server/modules/workspaces/events/eventListener.ts b/packages/server/modules/workspaces/events/eventListener.ts index 8bf84a527..c111f4d53 100644 --- a/packages/server/modules/workspaces/events/eventListener.ts +++ b/packages/server/modules/workspaces/events/eventListener.ts @@ -13,7 +13,9 @@ import { GetWorkspace, GetWorkspaceCollaborators, GetWorkspaceRoleForUser, + GetWorkspaceSeatsCount as GetWorkspaceSeatCount, GetWorkspaceSeatTypeToProjectRoleMapping, + GetWorkspacesProjectsCounts, QueryAllWorkspaceProjects, ValidateWorkspaceMemberProjectRole } from '@/modules/workspaces/domain/operations' @@ -32,6 +34,7 @@ import { WorkspaceInviteResourceType } from '@/modules/workspacesCore/domain/con import { MaybeNullOrUndefined, Roles, + SeatTypes, StreamRoles, throwUncoveredError, WorkspaceRoles @@ -45,6 +48,8 @@ import { getWorkspaceFactory, getWorkspaceRoleForUserFactory, getWorkspaceRolesFactory, + getWorkspaceSeatCountFactory, + getWorkspacesProjectsCountsFactory, getWorkspaceWithDomainsFactory, upsertWorkspaceRoleFactory } from '@/modules/workspaces/repositories/workspaces' @@ -114,6 +119,9 @@ import { getUserFactory } from '@/modules/core/repositories/users' import { authorizeResolver } from '@/modules/shared' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { getProjectWorkspaceFactory } from '@/modules/workspaces/repositories/projects' +import { GetWorkspaceModelCount } from '@speckle/shared/dist/commonjs/authz/domain/workspaces/operations.js' +import { getWorkspaceModelCountFactory } from '@/modules/workspaces/services/workspaceLimits' +import { getPaginatedProjectModelsTotalCountFactory } from '@/modules/core/repositories/branches' const { FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags() @@ -486,7 +494,10 @@ export const workspaceTrackingFactory = getDefaultRegion, getWorkspacePlan, getWorkspaceSubscription, - getUserEmails + getUserEmails, + getWorkspaceModelCount, + getWorkspacesProjectCount, + getWorkspaceSeatCount }: { getWorkspace: GetWorkspace countWorkspaceRole: CountWorkspaceRoleWithOptionalProjectRole @@ -494,6 +505,9 @@ export const workspaceTrackingFactory = getWorkspacePlan: GetWorkspacePlan getWorkspaceSubscription: GetWorkspaceSubscription getUserEmails: FindEmailsByUserId + getWorkspaceModelCount: GetWorkspaceModelCount + getWorkspacesProjectCount: GetWorkspacesProjectsCounts + getWorkspaceSeatCount: GetWorkspaceSeatCount }) => async (params: EventPayload<'workspace.*'> | EventPayload<'gatekeeper.*'>) => { // temp ignoring tracking for this, if billing is not enabled @@ -504,27 +518,52 @@ export const workspaceTrackingFactory = if (!mixpanel) return const calculateProperties = async (workspace: Workspace) => { const workspaceId = workspace.id - const [adminCount, memberCount, guestCount, defaultRegion, plan, subscription] = - await Promise.all([ - countWorkspaceRole({ workspaceId, workspaceRole: Roles.Workspace.Admin }), - countWorkspaceRole({ workspaceId, workspaceRole: Roles.Workspace.Member }), - countWorkspaceRole({ workspaceId, workspaceRole: Roles.Workspace.Guest }), - getDefaultRegion({ workspaceId }), - getWorkspacePlan({ workspaceId }), - getWorkspaceSubscription({ workspaceId }) - ]) - const seats = subscription?.subscriptionData - ? calculateSubscriptionSeats({ - subscriptionData: subscription?.subscriptionData - }) - : 0 + const [ + adminCount, + memberCount, + guestCount, + seatsViewerCount, + seatsEditorCount, + defaultRegion, + plan, + subscription, + workspacesProjectCount, + modelCount + ] = await Promise.all([ + countWorkspaceRole({ workspaceId, workspaceRole: Roles.Workspace.Admin }), + countWorkspaceRole({ workspaceId, workspaceRole: Roles.Workspace.Member }), + countWorkspaceRole({ workspaceId, workspaceRole: Roles.Workspace.Guest }), + getWorkspaceSeatCount({ workspaceId, type: SeatTypes.Editor }), + getWorkspaceSeatCount({ workspaceId, type: SeatTypes.Viewer }), + getDefaultRegion({ workspaceId }), + getWorkspacePlan({ workspaceId }), + getWorkspaceSubscription({ workspaceId }), + getWorkspacesProjectCount({ workspaceIds: [workspaceId] }), + getWorkspaceModelCount({ workspaceId }) + ]) + + let seats = 0 + let subscriptionBillingInterval = null + let subscriptionCurrentBillingCycleEnd = null + let subscriptionCreatedAt = null + + if (subscription !== null) { + seats = calculateSubscriptionSeats({ + subscriptionData: subscription.subscriptionData + }) + + subscriptionBillingInterval = subscription.billingInterval + subscriptionCurrentBillingCycleEnd = subscription.currentBillingCycleEnd + subscriptionCreatedAt = subscription.createdAt + } + return { name: workspace.name, description: workspace.description, domainBasedMembershipProtectionEnabled: workspace.domainBasedMembershipProtectionEnabled, discoverabilityEnabled: workspace.discoverabilityEnabled, - defaultRegionKey: defaultRegion?.key, + defaultRegionKey: defaultRegion?.key || null, teamTotalCount: adminCount + memberCount + guestCount, teamAdminCount: adminCount, teamMemberCount: memberCount, @@ -532,10 +571,16 @@ export const workspaceTrackingFactory = planName: plan?.name || '', planStatus: plan?.status || '', planCreatedAt: plan?.createdAt, - subscriptionBillingInterval: subscription?.billingInterval, - subscriptionCurrentBillingCycleEnd: subscription?.currentBillingCycleEnd, + subscriptionCreatedAt, + subscriptionBillingInterval, + subscriptionCurrentBillingCycleEnd, seats, seatsGuest: 0, + seatsViewerCount, + seatsEditorCount, + createdAt: workspace.createdAt, + projectCount: workspacesProjectCount[workspace.id] || 0, + modelCount, ...getBaseTrackingProperties() } } @@ -741,7 +786,16 @@ export const initializeEventListenersFactory = getUserEmails: findEmailsByUserIdFactory({ db }), getWorkspace: getWorkspaceFactory({ db }), getWorkspacePlan, - getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db }) + getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db }), + getWorkspaceModelCount: getWorkspaceModelCountFactory({ + queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ + getStreams + }), + getPaginatedProjectModelsTotalCount: + getPaginatedProjectModelsTotalCountFactory({ db }) + }), + getWorkspacesProjectCount: getWorkspacesProjectsCountsFactory({ db }), + getWorkspaceSeatCount: getWorkspaceSeatCountFactory({ db }) })(payload) }), eventBus.listen('gatekeeper.*', async (payload) => { @@ -751,7 +805,16 @@ export const initializeEventListenersFactory = getUserEmails: findEmailsByUserIdFactory({ db }), getWorkspace: getWorkspaceFactory({ db }), getWorkspacePlan, - getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db }) + getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db }), + getWorkspaceModelCount: getWorkspaceModelCountFactory({ + queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ + getStreams + }), + getPaginatedProjectModelsTotalCount: + getPaginatedProjectModelsTotalCountFactory({ db }) + }), + getWorkspacesProjectCount: getWorkspacesProjectsCountsFactory({ db }), + getWorkspaceSeatCount: getWorkspaceSeatCountFactory({ db }) })(payload) }), eventBus.listen(WorkspaceEvents.Authorizing, async ({ payload }) => { diff --git a/packages/server/modules/workspaces/repositories/workspaces.ts b/packages/server/modules/workspaces/repositories/workspaces.ts index fa9065b57..ab81e0a7a 100644 --- a/packages/server/modules/workspaces/repositories/workspaces.ts +++ b/packages/server/modules/workspaces/repositories/workspaces.ts @@ -28,6 +28,7 @@ import { GetWorkspaceRoleForUser, GetWorkspaceRoles, GetWorkspaceRolesForUser, + GetWorkspaceSeatsCount, GetWorkspaceWithDomains, GetWorkspaces, GetWorkspacesProjectsCounts, @@ -51,7 +52,8 @@ import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace import { WorkspaceAcl as DbWorkspaceAcl, WorkspaceDomains, - Workspaces + Workspaces, + WorkspaceSeats } from '@/modules/workspaces/helpers/db' import { knex, ServerAcl, StreamAcl, Streams, Users } from '@/modules/core/dbSchema' import { removePrivateFields } from '@/modules/core/helpers/userHelper' @@ -502,6 +504,21 @@ export const countWorkspaceRoleWithOptionalProjectRoleFactory = return parseInt(res.count.toString()) } +export const getWorkspaceSeatCountFactory = + ({ db }: { db: Knex }): GetWorkspaceSeatsCount => + async ({ workspaceId, type }) => { + const query = db(WorkspaceSeats.name).where( + WorkspaceSeats.col.workspaceId, + workspaceId + ) + + if (type) query.andWhere(WorkspaceSeats.col.type, type) + + const [{ count }] = await query.count() + + return parseInt(String(count)) + } + export const getWorkspaceCreationStateFactory = ({ db }: { db: Knex }): GetWorkspaceCreationState => async ({ workspaceId }) => {