Files
speckle-server/packages/server/modules/shared/utils/mixpanel.ts
T
2025-06-05 12:41:05 +02:00

143 lines
4.3 KiB
TypeScript

import { MaybeNullOrUndefined, Optional, resolveMixpanelUserId } from '@speckle/shared'
import * as MixpanelUtils from '@speckle/shared/observability/mixpanel'
import {
enableMixpanel,
getServerOrigin,
getServerVersion
} from '@/modules/shared/helpers/envHelper'
import Mixpanel from 'mixpanel'
import type express from 'express'
import type http from 'http'
import { mixpanelLogger } from '@/observability/logging'
import { WorkspacePlanStatuses } from '@/modules/cross-server-sync/graph/generated/graphql'
let client: Optional<MixpanelClient> = undefined
let baseTrackingProperties: Optional<Record<string, string>> = undefined
export const WORKSPACE_TRACKING_ID_KEY = 'workspace_id' as const
export const SERVER_TRACKING_ID_KEY = 'server_id' as const
export const USER_TRACKING_ID_KEY = 'distinct_id' as const
type MixpanelEvent = (typeof MixpanelEvents)[keyof typeof MixpanelEvents]
export const MixpanelEvents = {
WorkspaceUpgraded: 'Workspace Upgraded',
WorkspaceCreated: 'Workspace Created',
WorkspaceDeleted: 'Workspace Deleted',
WorkspaceSubscriptionCanceled: 'Workspace Subscription Canceled',
WorkspaceSubscriptionCancelationScheduled:
'Workspace Subscription Cancelation Scheduled',
WorkspaceSubscriptionPaymentFailed: 'Workspace Subscription Payment Failed',
FileUploadStarted: 'File Upload Started',
AutomateFunctionRunFinished: 'Automate Function Run Finished',
AutomationRunTriggered: 'Automation Run Triggered',
SignUp: 'Sign Up',
EditorSeatsPurchased: 'Editor Seats Purchased',
EditorSeatsDownscaled: 'Editor Seats Downscaled',
EditorSeatAssigned: 'Editor Seat Assigned',
EditorSeatUnassigned: 'Editor Seat Unassigned'
} as const
export const mapPlanStatusToMixpanelEvent = {
[WorkspacePlanStatuses.CancelationScheduled]:
MixpanelEvents.WorkspaceSubscriptionCancelationScheduled,
[WorkspacePlanStatuses.Canceled]: MixpanelEvents.WorkspaceSubscriptionCanceled,
[WorkspacePlanStatuses.PaymentFailed]:
MixpanelEvents.WorkspaceSubscriptionPaymentFailed
} as const
type TrackParameters = {
eventName: MixpanelEvent
payload?: Mixpanel.PropertyDict
workspaceId?: MaybeNullOrUndefined<string>
userEmail?: MaybeNullOrUndefined<string>
req?: Optional<express.Request | http.IncomingMessage>
}
export function getBaseTrackingProperties() {
if (baseTrackingProperties) return baseTrackingProperties
baseTrackingProperties = MixpanelUtils.buildBasePropertiesPayload({
hostApp: 'serverside',
serverOrigin: getServerOrigin(),
speckleVersion: getServerVersion()
})
return baseTrackingProperties
}
export function initialize() {
if (client || !enableMixpanel()) return
const mixpanel = MixpanelUtils.buildServerMixpanelClient({
tokenId: 'acd87c5a50b56df91a795e999812a3a4',
apiHostname: 'analytics.speckle.systems'
})
/**
* An abstraction layer over the track method that adds the id keys
* for the specified common objects
*/
const mixpanelTrack = mixpanel.track
const overwrittenTrack = async ({
eventName,
payload,
workspaceId,
userEmail,
req
}: TrackParameters) => {
const logger = req?.log || mixpanelLogger
const mixpanelUserId = userEmail?.length
? resolveMixpanelUserId(userEmail)
: undefined
const context = {
...(workspaceId && { [WORKSPACE_TRACKING_ID_KEY]: workspaceId }),
...MixpanelUtils.buildPropertiesPayload({
distinctId: mixpanelUserId,
query: (req && 'query' in req ? req?.query : {}) || {},
headers: req?.headers || {},
remoteAddress: req?.socket?.remoteAddress
}),
...getBaseTrackingProperties()
}
return new Promise<void>((resolve, reject) => {
mixpanelTrack(
eventName,
{
...payload,
...context
},
(err) => {
logger.info(
{
eventName,
payload,
...(err ? { err } : {})
},
'Mixpanel track() invoked'
)
if (err) return reject(err)
resolve()
}
)
})
}
client = {
...mixpanel,
track: overwrittenTrack
}
}
export function getClient() {
return client
}
export const getMixpanelClient = getClient
export type MixpanelClient = Omit<Mixpanel.Mixpanel, 'track'> & {
track: (args: TrackParameters) => Promise<void>
}
export { resolveMixpanelUserId }