diff --git a/.env.example b/.env.example index a2f6a4567bd..47979b3faab 100644 --- a/.env.example +++ b/.env.example @@ -62,13 +62,7 @@ CSP_REPORT_ONLY=false # - Set the log level (debug: most verbose, info: recommended for production, error: least verbose) # LOG_LEVEL=info -# -# -# - Telemetry is minimal, anonymous and helps us improve (set to false to disable). -# -# TELEMETRY=false -# -# + ############################################################################### TZ=UTC @@ -115,4 +109,4 @@ FLAG_SERVE_CONNECT_UI=true # ---- AWS # AWS_ACCESS_KEY_ID= -# AWS_SECRET_ACCESS_KEY= \ No newline at end of file +# AWS_SECRET_ACCESS_KEY= diff --git a/docker-compose.yaml b/docker-compose.yaml index a7d8734d172..5b323b6257f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -39,7 +39,6 @@ services: - NANGO_DASHBOARD_USERNAME - NANGO_DASHBOARD_PASSWORD - LOG_LEVEL=${LOG_LEVEL:-info} - - TELEMETRY - NANGO_SERVER_WEBSOCKETS_PATH - NANGO_LOGS_ENABLED=${NANGO_LOGS_ENABLED:-false} - NANGO_LOGS_ES_URL=${NANGO_LOGS_ES_URL:-http://nango-elasticsearch:9200} diff --git a/packages/persist/lib/server.integration.test.ts b/packages/persist/lib/server.integration.test.ts index 5f240afdc90..94506eb4f26 100644 --- a/packages/persist/lib/server.integration.test.ts +++ b/packages/persist/lib/server.integration.test.ts @@ -461,11 +461,9 @@ const initDb = async () => { const connectionRes = await connectionService.upsertConnection({ connectionId: `conn-test`, providerConfigKey: `provider-test`, - provider: 'google', parsedRawCredentials: {} as AuthCredentials, connectionConfig: {}, - environmentId: env.id, - accountId: 0 + environmentId: env.id }); const connectionId = connectionRes[0]?.connection.id; if (!connectionId) throw new Error('Connection not created'); diff --git a/packages/server/lib/controllers/appAuth.controller.ts b/packages/server/lib/controllers/appAuth.controller.ts index 3e2ab9e7460..95e1cc582c9 100644 --- a/packages/server/lib/controllers/appAuth.controller.ts +++ b/packages/server/lib/controllers/appAuth.controller.ts @@ -1,6 +1,6 @@ import db from '@nangohq/database'; import { logContextGetter } from '@nangohq/logs'; -import { AnalyticsTypes, analytics, configService, connectionService, environmentService, errorManager, getProvider, linkConnection } from '@nangohq/shared'; +import { configService, connectionService, environmentService, errorManager, getProvider, linkConnection } from '@nangohq/shared'; import { stringifyError } from '@nangohq/utils'; import publisher from '../clients/publisher.client.js'; @@ -50,8 +50,6 @@ class AppAuthController { const { environment, account } = environmentAndAccountLookup; - void analytics.track(AnalyticsTypes.PRE_APP_AUTH, account.id); - const { providerConfigKey, connectionId: receivedConnectionId, webSocketClientId: wsClientId } = session; const logCtx = logContextGetter.get({ id: session.activityLogId, accountId: account.id }); @@ -156,11 +154,9 @@ class AppAuthController { const [updatedConnection] = await connectionService.upsertConnection({ connectionId, providerConfigKey, - provider: session.provider, parsedRawCredentials: credentials as unknown as AuthCredentials, connectionConfig, - environmentId: environment.id, - accountId: account.id + environmentId: environment.id }); if (!updatedConnection) { void logCtx.error('Failed to create connection'); diff --git a/packages/server/lib/controllers/auth/postApiKey.ts b/packages/server/lib/controllers/auth/postApiKey.ts index 218f6b7715e..a8ec9ce5906 100644 --- a/packages/server/lib/controllers/auth/postApiKey.ts +++ b/packages/server/lib/controllers/auth/postApiKey.ts @@ -3,10 +3,8 @@ import { z } from 'zod'; import db from '@nangohq/database'; import { defaultOperationExpiration, endUserToMeta, logContextGetter } from '@nangohq/logs'; import { - AnalyticsTypes, ErrorSourceEnum, LogActionEnum, - analytics, configService, connectionService, errorManager, @@ -101,7 +99,6 @@ export const postPublicApiKeyAuthorization = asyncWrapper>, next: NextFunction) { try { const environmentId = res.locals['environment'].id; - const accountId = res.locals['account'].id; if (req.body == null) { errorManager.errRes(res, 'missing_body'); @@ -352,7 +349,6 @@ class ConfigController { const result = await configService.createProviderConfig(config, provider); if (result) { - void analytics.track(AnalyticsTypes.CONFIG_CREATED, accountId, { provider: result.provider }); res.status(200).send({ config: { unique_key: result.unique_key, diff --git a/packages/server/lib/controllers/connection.controller.ts b/packages/server/lib/controllers/connection.controller.ts index 661fc9c649a..f008c930e84 100644 --- a/packages/server/lib/controllers/connection.controller.ts +++ b/packages/server/lib/controllers/connection.controller.ts @@ -165,6 +165,7 @@ class ConnectionController { providerConfigKey: provider_config_key, environmentId: environment.id, creationType: 'import', + team: account, plan }); if (isCapped) { @@ -252,10 +253,8 @@ class ConnectionController { const [imported] = await connectionService.importOAuthConnection({ connectionId, providerConfigKey: provider_config_key, - provider: providerName, metadata, environment, - account, connectionConfig, parsedRawCredentials: oAuthCredentials, connectionCreatedHook: connCreatedHook @@ -317,10 +316,8 @@ class ConnectionController { const [imported] = await connectionService.importOAuthConnection({ connectionId, providerConfigKey: provider_config_key, - provider: providerName, metadata, environment, - account, connectionConfig, parsedRawCredentials: oAuthCredentials, connectionCreatedHook: connCreatedHook @@ -368,10 +365,8 @@ class ConnectionController { const [imported] = await connectionService.importOAuthConnection({ connectionId, providerConfigKey: provider_config_key, - provider: providerName, metadata, environment, - account, connectionConfig: { ...connection_config }, parsedRawCredentials: oAuthCredentials, connectionCreatedHook: connCreatedHook @@ -415,7 +410,6 @@ class ConnectionController { provider: providerName, metadata, environment, - account, credentials, connectionConfig: { ...connection_config }, connectionCreatedHook: connCreatedHook @@ -459,7 +453,6 @@ class ConnectionController { provider: providerName, metadata, environment, - account, connectionConfig: { ...connection_config }, credentials, connectionCreatedHook: connCreatedHook @@ -503,11 +496,9 @@ class ConnectionController { const [imported] = await connectionService.upsertConnection({ connectionId, providerConfigKey: provider_config_key, - provider: providerName, parsedRawCredentials: credentials as unknown as AuthCredentials, connectionConfig, environmentId: environment.id, - accountId: account.id, metadata }); @@ -559,8 +550,7 @@ class ConnectionController { }, metadata, config, - environment, - account + environment }); if (imported) { @@ -571,9 +561,7 @@ class ConnectionController { const [imported] = await connectionService.upsertUnauthConnection({ connectionId, providerConfigKey: provider_config_key, - provider: providerName, environment, - account, metadata, connectionConfig: { ...connection_config } }); diff --git a/packages/server/lib/controllers/connection/connectionId/getConnection.ts b/packages/server/lib/controllers/connection/connectionId/getConnection.ts index 2b344b227e5..e1bf2d266a2 100644 --- a/packages/server/lib/controllers/connection/connectionId/getConnection.ts +++ b/packages/server/lib/controllers/connection/connectionId/getConnection.ts @@ -1,12 +1,15 @@ import { z } from 'zod'; -import { asyncWrapper } from '../../../utils/asyncWrapper.js'; -import { metrics, zodErrorToHTTP } from '@nangohq/utils'; -import type { GetPublicConnection } from '@nangohq/types'; -import { connectionService, configService, refreshOrTestCredentials } from '@nangohq/shared'; -import { connectionRefreshFailed as connectionRefreshFailedHook, connectionRefreshSuccess as connectionRefreshSuccessHook } from '../../../hooks/hooks.js'; + import { logContextGetter } from '@nangohq/logs'; -import { connectionIdSchema, providerConfigKeySchema, stringBool } from '../../../helpers/validation.js'; +import { configService, connectionService, refreshOrTestCredentials } from '@nangohq/shared'; +import { metrics, zodErrorToHTTP } from '@nangohq/utils'; + import { connectionFullToPublicApi } from '../../../formatters/connection.js'; +import { connectionIdSchema, providerConfigKeySchema, stringBool } from '../../../helpers/validation.js'; +import { connectionRefreshFailed as connectionRefreshFailedHook, connectionRefreshSuccess as connectionRefreshSuccessHook } from '../../../hooks/hooks.js'; +import { asyncWrapper } from '../../../utils/asyncWrapper.js'; + +import type { GetPublicConnection } from '@nangohq/types'; const queryStringValidation = z .object({ diff --git a/packages/server/lib/controllers/connection/getConnections.ts b/packages/server/lib/controllers/connection/getConnections.ts index 03e41cccad0..80cfb6fc50c 100644 --- a/packages/server/lib/controllers/connection/getConnections.ts +++ b/packages/server/lib/controllers/connection/getConnections.ts @@ -1,11 +1,14 @@ -import { asyncWrapper } from '../../utils/asyncWrapper.js'; +import { z } from 'zod'; + +import { connectionService } from '@nangohq/shared'; import { zodErrorToHTTP } from '@nangohq/utils'; -import type { GetPublicConnections } from '@nangohq/types'; -import { AnalyticsTypes, analytics, connectionService } from '@nangohq/shared'; + import { connectionSimpleToPublicApi } from '../../formatters/connection.js'; -import { z } from 'zod'; +import { asyncWrapper } from '../../utils/asyncWrapper.js'; import { bodySchema } from '../connect/postSessions.js'; +import type { GetPublicConnections } from '@nangohq/types'; + const validationQuery = z .object({ connectionId: z.string().min(1).max(255).optional(), @@ -24,10 +27,9 @@ export const getPublicConnections = asyncWrapper(async (re return; } - const { environment, account } = res.locals; + const { environment } = res.locals; const queryParam: GetPublicConnections['Querystring'] = queryParamValues.data; - void analytics.track(AnalyticsTypes.CONNECTION_LIST_FETCHED, account.id); const connections = await connectionService.listConnections({ environmentId: environment.id, connectionId: queryParam.connectionId, diff --git a/packages/server/lib/controllers/oauth.controller.ts b/packages/server/lib/controllers/oauth.controller.ts index 53284837482..3a7ccd1f501 100644 --- a/packages/server/lib/controllers/oauth.controller.ts +++ b/packages/server/lib/controllers/oauth.controller.ts @@ -6,10 +6,8 @@ import * as uuid from 'uuid'; import db from '@nangohq/database'; import { defaultOperationExpiration, endUserToMeta, logContextGetter } from '@nangohq/logs'; import { - AnalyticsTypes, ErrorSourceEnum, LogActionEnum, - analytics, configService, connectionService, environmentService, @@ -52,7 +50,6 @@ import type { NextFunction, Request, Response } from 'express'; class OAuthController { public async oauthRequest(req: Request, res: Response>, _next: NextFunction) { const { account, environment, connectSession } = res.locals; - const accountId = account.id; const environmentId = environment.id; const { providerConfigKey } = req.params; const receivedConnectionId = req.query['connection_id'] as string | undefined; @@ -80,9 +77,6 @@ class OAuthController { }, { account, environment } ); - if (!wsClientId) { - void analytics.track(AnalyticsTypes.PRE_WS_OAUTH, accountId); - } const callbackUrl = await environmentService.getOauthCallbackUrl(environmentId); const connectionConfig = req.query['params'] != null ? getConnectionConfig(req.query['params']) : {}; @@ -322,7 +316,6 @@ class OAuthController { }, { account, environment } ); - void analytics.track(AnalyticsTypes.PRE_OAUTH2_CC_AUTH, account.id); if (!providerConfigKey) { errorManager.errRes(res, 'missing_connection'); @@ -427,11 +420,9 @@ class OAuthController { const [updatedConnection] = await connectionService.upsertConnection({ connectionId, providerConfigKey, - provider: config.provider, parsedRawCredentials: credentials, connectionConfig, - environmentId: environment.id, - accountId: account.id + environmentId: environment.id }); if (!updatedConnection) { res.status(500).send({ error: { code: 'server_error', message: 'failed to create connection' } }); @@ -1120,11 +1111,9 @@ class OAuthController { const [updatedConnection] = await connectionService.upsertConnection({ connectionId, providerConfigKey, - provider: session.provider, parsedRawCredentials, connectionConfig, - environmentId: session.environmentId, - accountId: account.id + environmentId: session.environmentId }); if (!updatedConnection) { void logCtx.error('Failed to create connection'); @@ -1311,11 +1300,9 @@ class OAuthController { const [updatedConnection] = await connectionService.upsertConnection({ connectionId, providerConfigKey, - provider: session.provider, parsedRawCredentials: parsedAccessTokenResult, connectionConfig, - environmentId: environment.id, - accountId: account.id + environmentId: environment.id }); if (!updatedConnection) { void logCtx.error('Failed to create connection'); diff --git a/packages/server/lib/controllers/sync.controller.ts b/packages/server/lib/controllers/sync.controller.ts index aa87a4eb04d..167abf327b2 100644 --- a/packages/server/lib/controllers/sync.controller.ts +++ b/packages/server/lib/controllers/sync.controller.ts @@ -1,39 +1,40 @@ -import type { Request, Response, NextFunction } from 'express'; -import type { HTTP_METHOD, Sync } from '@nangohq/shared'; import tracer from 'dd-trace'; -import type { Span } from 'dd-trace'; + +import { OtlpSpan, defaultOperationExpiration, logContextGetter } from '@nangohq/logs'; +import { getInterval } from '@nangohq/nango-yaml'; +import { records as recordsService } from '@nangohq/records'; import { - connectionService, - getSyncs, - verifyOwnership, - getSyncsByProviderConfigKey, - getSyncConfigsWithConnectionsByEnvironmentId, - SyncCommand, - errorManager, - analytics, - AnalyticsTypes, NangoError, + SyncCommand, configService, - syncManager, - getAttributes, + connectionService, + errorManager, flowService, getActionOrModelByEndpoint, - setFrequency, + getAttributes, getSyncAndActionConfigsBySyncNameAndConfigId, - syncCommandToOperation, getSyncConfigRaw, - getSyncsByConnectionId + getSyncConfigsWithConnectionsByEnvironmentId, + getSyncs, + getSyncsByConnectionId, + getSyncsByProviderConfigKey, + setFrequency, + syncCommandToOperation, + syncManager, + verifyOwnership } from '@nangohq/shared'; -import type { LogContextOrigin } from '@nangohq/logs'; -import { defaultOperationExpiration, logContextGetter, OtlpSpan } from '@nangohq/logs'; -import type { Result } from '@nangohq/utils'; -import { getHeaders, isHosted, truncateJson, Ok, Err, redactHeaders } from '@nangohq/utils'; -import { records as recordsService } from '@nangohq/records'; -import type { RequestLocals } from '../utils/express.js'; +import { Err, Ok, getHeaders, isHosted, redactHeaders, truncateJson } from '@nangohq/utils'; + import { getOrchestrator } from '../utils/utils.js'; -import { getInterval } from '@nangohq/nango-yaml'; import { getPublicRecords } from './records/getRecords.js'; + +import type { RequestLocals } from '../utils/express.js'; +import type { LogContextOrigin } from '@nangohq/logs'; +import type { HTTP_METHOD, Sync } from '@nangohq/shared'; import type { DBConnectionDecrypted } from '@nangohq/types'; +import type { Result } from '@nangohq/utils'; +import type { Span } from 'dd-trace'; +import type { NextFunction, Request, Response } from 'express'; const orchestrator = getOrchestrator(); @@ -434,7 +435,7 @@ class SyncController { try { const { account, environment } = res.locals; - const { schedule_id, command, nango_connection_id, sync_id, sync_name, sync_variant, provider, delete_records } = req.body; + const { command, nango_connection_id, sync_id, sync_name, sync_variant, delete_records } = req.body; const connection = await connectionService.getConnectionById(nango_connection_id); if (!connection) { res.status(404).json({ error: { code: 'not_found' } }); @@ -493,32 +494,6 @@ class SyncController { void logCtx.info(`Sync command run successfully "${command}"`, { command, syncId: sync_id }); await logCtx.success(); - let event = AnalyticsTypes.SYNC_RUN; - - switch (command) { - case SyncCommand.PAUSE: - event = AnalyticsTypes.SYNC_PAUSE; - break; - case SyncCommand.UNPAUSE: - event = AnalyticsTypes.SYNC_UNPAUSE; - break; - case SyncCommand.RUN: - event = AnalyticsTypes.SYNC_RUN; - break; - case SyncCommand.CANCEL: - event = AnalyticsTypes.SYNC_CANCEL; - break; - } - - void analytics.trackByEnvironmentId(event, environment.id, { - sync_id, - sync_name, - provider, - provider_config_key: connection?.provider_config_key, - connection_id: connection?.connection_id, - schedule_id - }); - res.status(200).json({ data: { success: true } }); } catch (err) { if (logCtx) { diff --git a/packages/server/lib/controllers/sync/deploy/postDeploy.ts b/packages/server/lib/controllers/sync/deploy/postDeploy.ts index 1a172ef5a05..ccef7c04016 100644 --- a/packages/server/lib/controllers/sync/deploy/postDeploy.ts +++ b/packages/server/lib/controllers/sync/deploy/postDeploy.ts @@ -1,7 +1,7 @@ import db from '@nangohq/database'; import { getLocking } from '@nangohq/kvstore'; import { logContextGetter } from '@nangohq/logs'; -import { AnalyticsTypes, NangoError, analytics, cleanIncomingFlow, deploy, errorManager, getAndReconcileDifferences, startTrial } from '@nangohq/shared'; +import { NangoError, cleanIncomingFlow, deploy, errorManager, getAndReconcileDifferences, productTracking, startTrial } from '@nangohq/shared'; import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; import { validationWithNangoYaml as validation } from './validation.js'; @@ -68,6 +68,7 @@ export const postDeploy = asyncWrapper(async (req, res) => { if (plan && !plan.trial_end_at && plan.name === 'free') { await startTrial(db.knex, plan); + productTracking.track({ name: 'account:trial:started', team: account }); } if (!success || !syncConfigDeployResult) { @@ -101,7 +102,7 @@ export const postDeploy = asyncWrapper(async (req, res) => { } } - void analytics.trackByEnvironmentId(AnalyticsTypes.SYNC_DEPLOY_SUCCESS, environment.id); + productTracking.track({ name: 'deploy:success', team: account }); if (lock) { await locking.release(lock); diff --git a/packages/server/lib/controllers/v1/account/managed/getCallback.ts b/packages/server/lib/controllers/v1/account/managed/getCallback.ts index 0fe7a4ba689..d7b64d40413 100644 --- a/packages/server/lib/controllers/v1/account/managed/getCallback.ts +++ b/packages/server/lib/controllers/v1/account/managed/getCallback.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; import db from '@nangohq/database'; -import { AnalyticsTypes, acceptInvitation, accountService, analytics, expirePreviousInvitations, getInvitation, userService } from '@nangohq/shared'; -import { basePublicUrl, getLogger, isCloud, nanoid } from '@nangohq/utils'; +import { acceptInvitation, accountService, expirePreviousInvitations, getInvitation, userService } from '@nangohq/shared'; +import { basePublicUrl, getLogger, nanoid } from '@nangohq/utils'; import { getWorkOSClient } from '../../../../clients/workos.client.js'; import { asyncWrapper } from '../../../../utils/asyncWrapper.js'; @@ -133,7 +133,6 @@ export const getManagedCallback = asyncWrapper(async (req, r return; } - void analytics.track(AnalyticsTypes.ACCOUNT_JOINED, invitation.account_id, {}, isCloud ? { email: invitation.email } : {}); // @ts-expect-error you got to love passport req.session.passport.user.account_id = invitation.account_id; } diff --git a/packages/server/lib/controllers/v1/account/signup.ts b/packages/server/lib/controllers/v1/account/signup.ts index 28bbaabacef..688705cb482 100644 --- a/packages/server/lib/controllers/v1/account/signup.ts +++ b/packages/server/lib/controllers/v1/account/signup.ts @@ -3,8 +3,8 @@ import util from 'util'; import { z } from 'zod'; -import { AnalyticsTypes, acceptInvitation, accountService, analytics, getInvitation, userService } from '@nangohq/shared'; -import { isCloud, requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; +import { acceptInvitation, accountService, getInvitation, userService } from '@nangohq/shared'; +import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; import { sendVerificationEmail } from '../../../helpers/email.js'; import { asyncWrapper } from '../../../utils/asyncWrapper.js'; @@ -79,7 +79,6 @@ export const signup = asyncWrapper(async (req, res) => { res.status(500).send({ error: { code: 'server_error', message: 'Failed to get team' } }); return; } - void analytics.track(AnalyticsTypes.ACCOUNT_JOINED, account.id, {}, isCloud ? { email } : {}); await acceptInvitation(token); } else { diff --git a/packages/server/lib/controllers/v1/account/validateEmailAndLogin.ts b/packages/server/lib/controllers/v1/account/validateEmailAndLogin.ts index fe507b60bb8..c5d61b3efc8 100644 --- a/packages/server/lib/controllers/v1/account/validateEmailAndLogin.ts +++ b/packages/server/lib/controllers/v1/account/validateEmailAndLogin.ts @@ -1,9 +1,12 @@ import { z } from 'zod'; -import { asyncWrapper } from '../../../utils/asyncWrapper.js'; + +import { userService } from '@nangohq/shared'; import { getLogger, requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; -import { analytics, userService, AnalyticsTypes } from '@nangohq/shared'; -import type { ValidateEmailAndLogin } from '@nangohq/types'; + import { userToAPI } from '../../../formatters/user.js'; +import { asyncWrapper } from '../../../utils/asyncWrapper.js'; + +import type { ValidateEmailAndLogin } from '@nangohq/types'; const logger = getLogger('Server.ValidateEmailAndLogin'); @@ -57,10 +60,6 @@ export const validateEmailAndLogin = asyncWrapper(async ( await userService.verifyUserEmail(user.id); - const { account_id, email } = user; - - void analytics.track(AnalyticsTypes.ACCOUNT_CREATED, account_id, {}, { email }); - req.login(user, function (err) { if (err) { logger.error('Error logging in user'); diff --git a/packages/server/lib/controllers/v1/flows/id/patchEnable.ts b/packages/server/lib/controllers/v1/flows/id/patchEnable.ts index 17b33b49423..3a91a8fac5d 100644 --- a/packages/server/lib/controllers/v1/flows/id/patchEnable.ts +++ b/packages/server/lib/controllers/v1/flows/id/patchEnable.ts @@ -1,6 +1,6 @@ import db from '@nangohq/database'; import { logContextGetter } from '@nangohq/logs'; -import { configService, connectionService, enableScriptConfig, getSyncConfigById, startTrial, syncManager } from '@nangohq/shared'; +import { configService, connectionService, enableScriptConfig, getSyncConfigById, productTracking, startTrial, syncManager } from '@nangohq/shared'; import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; import { validationBody, validationParams } from './patchDisable.js'; @@ -34,7 +34,7 @@ export const patchFlowEnable = asyncWrapper(async (req, res) => } const body: PatchFlowEnable['Body'] = val.data; - const { environment, plan } = res.locals; + const { environment, account, plan, user } = res.locals; const syncConfig = await getSyncConfigById(environment.id, valParams.data.id); if (!syncConfig) { @@ -54,12 +54,15 @@ export const patchFlowEnable = asyncWrapper(async (req, res) => } if (plan && !plan.trial_end_at && plan.name === 'free') { await startTrial(db.knex, plan); + productTracking.track({ name: 'account:trial:started', team: account, user }); } const isCapped = await connectionService.shouldCapUsage({ providerConfigKey: body.providerConfigKey, environmentId: environment.id, type: 'activate', + team: account, + user, plan }); if (isCapped) { diff --git a/packages/server/lib/controllers/v1/flows/preBuilt/postDeploy.ts b/packages/server/lib/controllers/v1/flows/preBuilt/postDeploy.ts index 1fdbfce5119..e59a26286e5 100644 --- a/packages/server/lib/controllers/v1/flows/preBuilt/postDeploy.ts +++ b/packages/server/lib/controllers/v1/flows/preBuilt/postDeploy.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import db from '@nangohq/database'; import { logContextGetter } from '@nangohq/logs'; -import { configService, connectionService, deployPreBuilt, flowService, startTrial, syncManager } from '@nangohq/shared'; +import { configService, connectionService, deployPreBuilt, flowService, productTracking, startTrial, syncManager } from '@nangohq/shared'; import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; import { providerConfigKeySchema, providerSchema, scriptNameSchema } from '../../../../helpers/validation.js'; @@ -55,12 +55,14 @@ export const postPreBuiltDeploy = asyncWrapper(async (req, r } if (plan && !plan.trial_end_at && plan.name === 'free') { await startTrial(db.knex, plan); + productTracking.track({ name: 'account:trial:started', team: account }); } const isCapped = await connectionService.shouldCapUsage({ providerConfigKey: body.providerConfigKey, environmentId, type: 'deploy', + team: account, plan }); if (isCapped) { diff --git a/packages/server/lib/controllers/v1/integrations/postIntegration.ts b/packages/server/lib/controllers/v1/integrations/postIntegration.ts index fe7afd150f9..a0c8cbcee8f 100644 --- a/packages/server/lib/controllers/v1/integrations/postIntegration.ts +++ b/packages/server/lib/controllers/v1/integrations/postIntegration.ts @@ -1,10 +1,13 @@ import { z } from 'zod'; + +import { configService, getProvider } from '@nangohq/shared'; import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; -import { asyncWrapper } from '../../../utils/asyncWrapper.js'; -import type { PostIntegration } from '@nangohq/types'; -import { AnalyticsTypes, analytics, configService, getProvider } from '@nangohq/shared'; + import { integrationToApi } from '../../../formatters/integration.js'; import { providerSchema } from '../../../helpers/validation.js'; +import { asyncWrapper } from '../../../utils/asyncWrapper.js'; + +import type { PostIntegration } from '@nangohq/types'; const validationBody = z .object({ @@ -36,12 +39,10 @@ export const postIntegration = asyncWrapper(async (req, res) => return; } - const { environment, account } = res.locals; + const { environment } = res.locals; const integration = await configService.createEmptyProviderConfig(body.provider, environment.id, provider); - void analytics.track(AnalyticsTypes.CONFIG_CREATED, account.id, { provider: body.provider }); - res.status(200).send({ data: integrationToApi(integration) }); diff --git a/packages/server/lib/controllers/v1/invite/acceptInvite.ts b/packages/server/lib/controllers/v1/invite/acceptInvite.ts index 1ef410142b5..d9e60240e67 100644 --- a/packages/server/lib/controllers/v1/invite/acceptInvite.ts +++ b/packages/server/lib/controllers/v1/invite/acceptInvite.ts @@ -1,7 +1,10 @@ -import { AnalyticsTypes, acceptInvitation, analytics, getInvitation, userService } from '@nangohq/shared'; -import { asyncWrapper } from '../../../utils/asyncWrapper.js'; -import { isCloud, requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; import { z } from 'zod'; + +import { acceptInvitation, getInvitation, userService } from '@nangohq/shared'; +import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; + +import { asyncWrapper } from '../../../utils/asyncWrapper.js'; + import type { AcceptInvite } from '@nangohq/types'; const validation = z @@ -40,8 +43,6 @@ export const acceptInvite = asyncWrapper(async (req, res) => { return; } - void analytics.track(AnalyticsTypes.ACCOUNT_JOINED, invitation.account_id, {}, isCloud ? { email: invitation.email } : {}); - // User is stored in session, so we need to update the DB // @ts-expect-error you got to love passport req.session.passport.user = updated; diff --git a/packages/server/lib/controllers/v1/plan/postPlanExtendTrial.ts b/packages/server/lib/controllers/v1/plan/postPlanExtendTrial.ts index 2be5c831323..c7992baa893 100644 --- a/packages/server/lib/controllers/v1/plan/postPlanExtendTrial.ts +++ b/packages/server/lib/controllers/v1/plan/postPlanExtendTrial.ts @@ -1,5 +1,5 @@ import db from '@nangohq/database'; -import { startTrial } from '@nangohq/shared'; +import { productTracking, startTrial } from '@nangohq/shared'; import { flagHasPlan, requireEmptyBody, requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; import { asyncWrapper } from '../../../utils/asyncWrapper.js'; @@ -19,7 +19,7 @@ export const postPlanExtendTrial = asyncWrapper(async (req, return; } - const { plan } = res.locals; + const { plan, account, user } = res.locals; if (!flagHasPlan) { res.status(400).send({ error: { code: 'feature_disabled' } }); return; @@ -32,6 +32,8 @@ export const postPlanExtendTrial = asyncWrapper(async (req, await startTrial(db.knex, plan); + productTracking.track({ name: 'account:trial:extend', team: account, user }); + res.status(200).send({ data: { success: true } }); diff --git a/packages/server/lib/crons/trial.ts b/packages/server/lib/crons/trial.ts index 8365064e98e..c33d3f842b9 100644 --- a/packages/server/lib/crons/trial.ts +++ b/packages/server/lib/crons/trial.ts @@ -4,8 +4,6 @@ import * as cron from 'node-cron'; import db from '@nangohq/database'; import { getLocking } from '@nangohq/kvstore'; import { - AnalyticsTypes, - analytics, disableScriptConfig, environmentService, errorNotificationService, @@ -75,7 +73,6 @@ export async function exec(): Promise { await updatePlan(db.knex, { id: plan.id, trial_end_notified_at: new Date() }); logger.info('Trial soon to be over for account', plan.account_id); - void analytics.track(AnalyticsTypes.ACCOUNT_TRIAL_EXPIRING_MAIL, plan.account_id); const users = await userService.getUsersByAccountId(plan.account_id); @@ -116,7 +113,6 @@ export async function exec(): Promise { await updatePlan(db.knex, { id: plan.id, trial_expired: true }); - void analytics.track(AnalyticsTypes.ACCOUNT_TRIAL_EXPIRED, plan.account_id); const users = await userService.getUsersByAccountId(plan.account_id); // Send in parallel diff --git a/packages/server/lib/hooks/hooks.ts b/packages/server/lib/hooks/hooks.ts index 6854f57bb36..eb6125c3bd6 100644 --- a/packages/server/lib/hooks/hooks.ts +++ b/packages/server/lib/hooks/hooks.ts @@ -1,14 +1,13 @@ import tracer from 'dd-trace'; import { - AnalyticsTypes, NangoError, ProxyRequest, - analytics, errorNotificationService, externalWebhookService, getProxyConfiguration, getSyncConfigsWithConnections, + productTracking, syncManager } from '@nangohq/shared'; import { Err, Ok, getLogger, isHosted, report } from '@nangohq/utils'; @@ -48,11 +47,13 @@ export const connectionCreationStartCapCheck = async ({ providerConfigKey, environmentId, creationType, + team, plan }: { providerConfigKey: string | undefined; environmentId: number; creationType: 'create' | 'import'; + team: DBTeam; plan: DBPlan; }): Promise => { if (!providerConfigKey) { @@ -69,9 +70,11 @@ export const connectionCreationStartCapCheck = async ({ if (connections && connections.length >= plan.connection_with_scripts_max) { logger.info(`Reached cap for providerConfigKey: ${providerConfigKey} and environmentId: ${environmentId}`); - const analyticsType = - creationType === 'create' ? AnalyticsTypes.RESOURCE_CAPPED_CONNECTION_CREATED : AnalyticsTypes.RESOURCE_CAPPED_CONNECTION_IMPORTED; - void analytics.trackByEnvironmentId(analyticsType, environmentId); + if (creationType === 'create') { + productTracking.track({ name: 'server:resource_capped:connection_creation', team }); + } else { + productTracking.track({ name: 'server:resource_capped:connection_imported', team }); + } return true; } } diff --git a/packages/server/lib/middleware/resource-capping.middleware.ts b/packages/server/lib/middleware/resource-capping.middleware.ts index aa047ad8e7b..451dc1224e6 100644 --- a/packages/server/lib/middleware/resource-capping.middleware.ts +++ b/packages/server/lib/middleware/resource-capping.middleware.ts @@ -5,7 +5,7 @@ import type { ApiError, Endpoint } from '@nangohq/types'; export const resourceCapping = asyncWrapper }>>( async (req, res, next) => { - const { environment, plan } = res.locals; + const { environment, plan, account } = res.locals; const { providerConfigKey } = req.params; @@ -14,6 +14,7 @@ export const resourceCapping = asyncWrapper { const storedConnection = await this.checkIfConnectionExists(connectionId, providerConfigKey, environmentId); @@ -131,8 +127,6 @@ class ConnectionService { .update(encryptedConnection) .returning('*'); - void analytics.track(AnalyticsTypes.CONNECTION_UPDATED, accountId, { provider }); - return [{ connection: connection[0]!, operation: 'override' }]; } @@ -158,8 +152,6 @@ class ConnectionService { }); const connection = await db.knex.from(`_nango_connections`).insert(data).returning('*'); - void analytics.track(AnalyticsTypes.CONNECTION_INSERTED, accountId, { provider }); - return [{ connection: connection[0]!, operation: 'creation' }]; } @@ -170,8 +162,7 @@ class ConnectionService { connectionConfig, metadata, config, - environment, - account + environment }: { connectionId: string; providerConfigKey: string; @@ -188,7 +179,6 @@ class ConnectionService { config: ProviderConfig; metadata?: Metadata | null; environment: DBEnvironment; - account: DBTeam; }): Promise { const { id, ...encryptedConnection } = encryptionManager.encryptConnection({ connection_id: connectionId, @@ -236,33 +226,21 @@ class ConnectionService { const operation = connection ? 'creation' : 'override'; - if (credentials.type) { - await analytics.trackConnectionEvent({ - provider_type: credentials.type, - operation, - accountId: account.id - }); - } - return [{ connection: connection!, operation }]; } public async upsertUnauthConnection({ connectionId, providerConfigKey, - provider, metadata, connectionConfig, - environment, - account + environment }: { connectionId: string; providerConfigKey: string; - provider: string; metadata?: Metadata | null; connectionConfig?: ConnectionConfig; environment: DBEnvironment; - account: DBTeam; }): Promise { const storedConnection = await this.checkIfConnectionExists(connectionId, providerConfigKey, environment.id); const config_id = await configService.getIdByProviderConfigKey(environment.id, providerConfigKey); // TODO remove that @@ -287,8 +265,6 @@ class ConnectionService { }) .returning('*'); - void analytics.track(AnalyticsTypes.UNAUTH_CONNECTION_UPDATED, account.id, { provider }); - return [{ connection: connection[0]!, operation: 'override' }]; } const connection = await db.knex @@ -309,17 +285,13 @@ class ConnectionService { }) .returning('*'); - void analytics.track(AnalyticsTypes.UNAUTH_CONNECTION_INSERTED, account.id, { provider }); - return [{ connection: connection[0]!, operation: 'creation' }]; } public async importOAuthConnection({ connectionId, providerConfigKey, - provider, environment, - account, metadata = null, connectionConfig = {}, parsedRawCredentials, @@ -327,9 +299,7 @@ class ConnectionService { }: { connectionId: string; providerConfigKey: string; - provider: string; environment: DBEnvironment; - account: DBTeam; metadata?: Metadata | null; connectionConfig?: ConnectionConfig; parsedRawCredentials: OAuth2Credentials | OAuth1Credentials | OAuth2ClientCredentials; @@ -338,11 +308,9 @@ class ConnectionService { const [importedConnection] = await this.upsertConnection({ connectionId, providerConfigKey, - provider, parsedRawCredentials, connectionConfig, environmentId: environment.id, - accountId: account.id, metadata }); @@ -358,7 +326,6 @@ class ConnectionService { providerConfigKey, metadata = null, environment, - account, connectionConfig = {}, credentials, connectionCreatedHook @@ -367,7 +334,6 @@ class ConnectionService { providerConfigKey: string; provider: string; environment: DBEnvironment; - account: DBTeam; metadata?: Metadata | null; connectionConfig?: ConnectionConfig; credentials: BasicApiCredentials | ApiKeyCredentials; @@ -387,8 +353,7 @@ class ConnectionService { connectionConfig, metadata, config, - environment, - account + environment }); if (importedConnection) { @@ -1056,16 +1021,12 @@ class ConnectionService { return; } - const accountId = await environmentService.getAccountIdFromEnvironment(integration.environment_id); - const [updatedConnection] = await this.upsertConnection({ connectionId, providerConfigKey: integration.unique_key, - provider: integration.provider, parsedRawCredentials: credentials as unknown as AuthCredentials, connectionConfig, - environmentId: integration.environment_id, - accountId: accountId as number + environmentId: integration.environment_id }); if (updatedConnection) { @@ -1384,11 +1345,15 @@ class ConnectionService { providerConfigKey, environmentId, type, + team, + user, plan }: { providerConfigKey: string; environmentId: number; type: 'activate' | 'deploy'; + team: DBTeam; + user?: DBUser; plan: DBPlan | null; }): Promise { if (!plan || !plan.connection_with_scripts_max) { @@ -1400,9 +1365,9 @@ class ConnectionService { if (count > plan.connection_with_scripts_max) { logger.info(`Reached cap for providerConfigKey: ${providerConfigKey} and environmentId: ${environmentId}`); if (type === 'deploy') { - void analytics.trackByEnvironmentId(AnalyticsTypes.RESOURCE_CAPPED_SCRIPT_DEPLOY_IS_DISABLED, environmentId); + productTracking.track({ name: 'server:resource_capped:script_deploy_is_disabled', team, user }); } else { - void analytics.trackByEnvironmentId(AnalyticsTypes.RESOURCE_CAPPED_SCRIPT_ACTIVATE, environmentId); + productTracking.track({ name: 'server:resource_capped:script_activate', team, user }); } return true; } diff --git a/packages/shared/lib/services/plans/plans.ts b/packages/shared/lib/services/plans/plans.ts index e12c551d1c0..c92fb2223d8 100644 --- a/packages/shared/lib/services/plans/plans.ts +++ b/packages/shared/lib/services/plans/plans.ts @@ -2,8 +2,6 @@ import ms from 'ms'; import { Err, Ok } from '@nangohq/utils'; -import analytics, { AnalyticsTypes } from '../../utils/analytics.js'; - import type { DBPlan } from '@nangohq/types'; import type { Result } from '@nangohq/utils'; import type { Knex } from 'knex'; @@ -57,12 +55,6 @@ export async function updatePlan(db: Knex, { id, ...data }: Pick & } export async function startTrial(db: Knex, plan: DBPlan): Promise> { - if (plan.trial_start_at) { - void analytics.track(AnalyticsTypes.ACCOUNT_TRIAL_EXTENDED, plan.account_id); - } else { - void analytics.track(AnalyticsTypes.ACCOUNT_TRIAL_STARTED, plan.account_id); - } - return await updatePlan(db, { id: plan.id, trial_start_at: plan.trial_start_at || new Date(), diff --git a/packages/shared/lib/services/sync/config/deploy.service.ts b/packages/shared/lib/services/sync/config/deploy.service.ts index a96ab97c893..9b38263749e 100644 --- a/packages/shared/lib/services/sync/config/deploy.service.ts +++ b/packages/shared/lib/services/sync/config/deploy.service.ts @@ -729,6 +729,7 @@ async function compileDeployInfo({ providerConfigKey, environmentId: environment_id, type: 'deploy', + team: account, plan }); diff --git a/packages/shared/lib/utils/analytics.ts b/packages/shared/lib/utils/analytics.ts deleted file mode 100644 index 3dcdca541eb..00000000000 --- a/packages/shared/lib/utils/analytics.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { PostHog } from 'posthog-node'; - -import { NANGO_VERSION, baseUrl, getLogger, isCloud, isStaging, localhostUrl } from '@nangohq/utils'; - -import errorManager, { ErrorSourceEnum } from './error.manager.js'; -import { LogActionEnum } from '../models/Telemetry.js'; -import accountService from '../services/account.service.js'; -import environmentService from '../services/environment.service.js'; -import userService from '../services/user.service.js'; -import { UserType } from '../utils/utils.js'; - -const logger = getLogger('analytics'); - -export enum AnalyticsTypes { - ACCOUNT_CREATED = 'server:account_created', - ACCOUNT_JOINED = 'server:account_joined', - ACCOUNT_TRIAL_EXPIRING_MAIL = 'account:trial:expiring:mail', - ACCOUNT_TRIAL_EXPIRED = 'account:trial:expired', - ACCOUNT_TRIAL_EXTENDED = 'account:trial:extend', - ACCOUNT_TRIAL_STARTED = 'account:trial:started', - API_CONNECTION_INSERTED = 'server:api_key_connection_inserted', - API_CONNECTION_UPDATED = 'server:api_key_connection_updated', - TBA_CONNECTION_INSERTED = 'server:tba_connection_inserted', - TBA_CONNECTION_UPDATED = 'server:tba_connection_updated', - TABLEAU_CONNECTION_INSERTED = 'server:tableau_connection_inserted', - TABLEAU_CONNECTION_UPDATED = 'server:tableau_connection_updated', - JWT_CONNECTION_INSERTED = 'server:jwt_connection_inserted', - JWT_CONNECTION_UPDATED = 'server:jwt_connection_updated', - BILL_CONNECTION_INSERTED = 'server:bill_connection_inserted', - BILL_CONNECTION_UPDATED = 'server:bill_connection_updated', - TWO_STEP_CONNECTION_INSERTED = 'server:two_step_connection_inserted', - TWO_STEP_CONNECTION_UPDATED = 'server:two_step_connection_updated', - SIGNATURE_CONNECTION_INSERTED = 'server:signature_connection_inserted', - SIGNATURE_CONNECTION_UPDATED = 'server:signature_connection_updated', - CONFIG_CREATED = 'server:config_created', - CONNECTION_INSERTED = 'server:connection_inserted', - CONNECTION_LIST_FETCHED = 'server:connection_list_fetched', - CONNECTION_UPDATED = 'server:connection_updated', - PRE_API_KEY_AUTH = 'server:pre_api_key_auth', - PRE_APP_AUTH = 'server:pre_appauth', - PRE_APP_STORE_AUTH = 'server:pre_app_store_auth', - PRE_BASIC_API_KEY_AUTH = 'server:pre_basic_api_key_auth', - PRE_UNAUTH = 'server:pre_unauth', - PRE_WS_OAUTH = 'server:pre_ws_oauth', - PRE_BILL_AUTH = 'server:pre_bill_auth', - PRE_TWO_STEP_AUTH = 'server:pre_two_step_auth', - PRE_OAUTH2_CC_AUTH = 'server:pre_oauth2_cc_auth', - PRE_TBA_AUTH = 'server:pre_tba_auth', - PRE_JWT_AUTH = 'server:pre_jwt_auth', - PRE_SIGNATURE_AUTH = 'server:pre_signature_auth', - RESOURCE_CAPPED_CONNECTION_CREATED = 'server:resource_capped:connection_creation', - RESOURCE_CAPPED_CONNECTION_IMPORTED = 'server:resource_capped:connection_imported', - RESOURCE_CAPPED_SCRIPT_ACTIVATE = 'server:resource_capped:script_activate', - RESOURCE_CAPPED_SCRIPT_DEPLOY_IS_DISABLED = 'server:resource_capped:script_deploy_is_disabled', - SYNC_DEPLOY_SUCCESS = 'sync:deploy_succeeded', - SYNC_PAUSE = 'sync:command_pause', - SYNC_RUN = 'sync:command_run', - SYNC_UNPAUSE = 'sync:command_unpause', - SYNC_CANCEL = 'sync:command_cancel', - UNAUTH_CONNECTION_INSERTED = 'server:unauth_connection_inserted', - UNAUTH_CONNECTION_UPDATED = 'server:unauth_connection_updated', - WEB_CONNECION_CREATED = 'web:connection_created', - WEB_ACCOUNT_SIGNUP = 'web:account_signup' -} - -type OperationType = 'override' | 'creation'; -type ProviderType = 'SIGNATURE' | 'TWO_STEP' | 'BILL' | 'JWT' | 'TABLEAU' | 'TBA' | 'API_KEY' | 'BASIC'; - -const AnalyticsEventMapping: Record> = { - TWO_STEP: { - creation: AnalyticsTypes.TWO_STEP_CONNECTION_INSERTED, - override: AnalyticsTypes.TWO_STEP_CONNECTION_UPDATED - }, - SIGNATURE: { - creation: AnalyticsTypes.SIGNATURE_CONNECTION_INSERTED, - override: AnalyticsTypes.SIGNATURE_CONNECTION_UPDATED - }, - BILL: { - creation: AnalyticsTypes.BILL_CONNECTION_INSERTED, - override: AnalyticsTypes.BILL_CONNECTION_UPDATED - }, - JWT: { - creation: AnalyticsTypes.JWT_CONNECTION_INSERTED, - override: AnalyticsTypes.JWT_CONNECTION_UPDATED - }, - TABLEAU: { - creation: AnalyticsTypes.TABLEAU_CONNECTION_INSERTED, - override: AnalyticsTypes.TABLEAU_CONNECTION_UPDATED - }, - TBA: { - creation: AnalyticsTypes.TBA_CONNECTION_INSERTED, - override: AnalyticsTypes.TBA_CONNECTION_UPDATED - }, - API_KEY: { - creation: AnalyticsTypes.API_CONNECTION_INSERTED, - override: AnalyticsTypes.API_CONNECTION_UPDATED - }, - BASIC: { - creation: AnalyticsTypes.API_CONNECTION_INSERTED, - override: AnalyticsTypes.API_CONNECTION_UPDATED - } -}; - -class Analytics { - client: PostHog | undefined; - packageVersion: string | undefined; - - constructor() { - const hasTelemetry = process.env['TELEMETRY'] !== 'false' && !isStaging; - if (!hasTelemetry) { - return; - } - - // hardcoded for OSS telemetry - const key = process.env['PUBLIC_POSTHOG_KEY'] || 'phc_4S2pWFTyPYT1i7zwC8YYQqABvGgSAzNHubUkdEFvcTl'; - if (!key) { - logger.error('No PostHog key'); - return; - } - - try { - this.client = new PostHog(key); - this.client.enable(); - this.packageVersion = NANGO_VERSION; - } catch (err) { - errorManager.report(err, { - source: ErrorSourceEnum.PLATFORM, - operation: LogActionEnum.ANALYTICS - }); - } - } - - public async track(name: string, accountId: number, eventProperties?: Record, userProperties?: Record) { - try { - if (this.client == null) { - return; - } - - eventProperties = eventProperties || {}; - userProperties = userProperties || {}; - - const userType = this.getUserType(accountId, baseUrl); - const userId = this.getUserIdWithType(userType, accountId, baseUrl); - - eventProperties['host'] = baseUrl; - eventProperties['user-type'] = userType; - eventProperties['user-account'] = userId; - eventProperties['nango-server-version'] = this.packageVersion || 'unknown'; - - if (isCloud && accountId != null) { - const account = await accountService.getAccountById(accountId); - if (account !== null && account.id !== undefined) { - const users = await userService.getUsersByAccountId(account.id); - - if (users.length > 0) { - userProperties['email'] = users.map((user) => user.email).join(','); - userProperties['name'] = users.map((user) => user.name).join(','); - } - } - } - - userProperties['user-type'] = userType; - userProperties['account'] = userId; - eventProperties['$set'] = userProperties; - - this.client.capture({ - event: name, - distinctId: userId, - properties: eventProperties - }); - } catch (err) { - errorManager.report(err, { - source: ErrorSourceEnum.PLATFORM, - operation: LogActionEnum.ANALYTICS, - accountId: accountId - }); - } - } - - public async trackByEnvironmentId( - name: string, - environmentId: number, - eventProperties?: Record, - userProperties?: Record - ) { - const accountId = await environmentService.getAccountIdFromEnvironment(environmentId); - if (typeof accountId !== 'undefined' && accountId !== null) { - return this.track(name, accountId, eventProperties, userProperties); - } - } - - public getUserType(accountId: number, baseUrl: string): UserType { - if (baseUrl === localhostUrl) { - return UserType.Local; - } else if (accountId === 0) { - return UserType.SelfHosted; - } else { - return UserType.Cloud; - } - } - - public getUserIdWithType(userType: string, accountId: number, baseUrl: string): string { - switch (userType) { - case UserType.Local: - return `${userType}-local`; - case UserType.SelfHosted: - return `${userType}-${baseUrl}`; - case UserType.Cloud: - return `${userType}-${(accountId || 0).toString()}`; - default: - return 'unknown'; - } - } - - public async trackConnectionEvent({ - provider_type, - operation, - accountId - }: { - provider_type: string; - operation: OperationType; - accountId: number; - }): Promise { - const providerKey = provider_type as ProviderType; - - const eventType = AnalyticsEventMapping[providerKey][operation]; - - await this.track(eventType, accountId, { provider_type }); - } -} - -export default new Analytics(); diff --git a/packages/shared/lib/utils/productTracking.ts b/packages/shared/lib/utils/productTracking.ts new file mode 100644 index 00000000000..8c2388d100b --- /dev/null +++ b/packages/shared/lib/utils/productTracking.ts @@ -0,0 +1,79 @@ +import { PostHog } from 'posthog-node'; + +import { NANGO_VERSION, baseUrl, report } from '@nangohq/utils'; + +import type { DBTeam, DBUser } from '@nangohq/types'; + +export type ProductTrackingTypes = + | 'account:trial:extend' + | 'account:trial:started' + | 'server:resource_capped:connection_creation' + | 'server:resource_capped:connection_imported' + | 'server:resource_capped:script_activate' + | 'server:resource_capped:script_deploy_is_disabled' + | 'deploy:success'; + +class ProductTracking { + client: PostHog | undefined; + + constructor() { + const key = process.env['PUBLIC_POSTHOG_KEY']; + if (!key) { + return; + } + + try { + this.client = new PostHog(key, { + host: process.env['PUBLIC_POSTHOG_HOST'] || 'https://app.posthog.com' + }); + this.client.enable(); + } catch (err) { + report(err); + } + } + + public track({ + name, + team, + user, + eventProperties, + userProperties + }: { + name: ProductTrackingTypes; + team: Pick; + user?: Pick | undefined; + eventProperties?: Record; + userProperties?: Record; + }) { + try { + if (this.client == null) { + return; + } + + eventProperties = eventProperties || {}; + userProperties = userProperties || {}; + + eventProperties['host'] = baseUrl; + eventProperties['nango-server-version'] = NANGO_VERSION || 'unknown'; + + eventProperties['team-id'] = team.id; + eventProperties['team-name'] = team.name; + userProperties['team-id'] = team.id; + userProperties['team-name'] = team.name; + let distinctId = `team-${team.id}`; + if (user) { + userProperties['email'] = user.email; + userProperties['name'] = user.name; + userProperties['id'] = user.id; + distinctId += `-user-${user.id}`; + } + + eventProperties['$set'] = userProperties; + this.client.capture({ event: name, distinctId, properties: eventProperties }); + } catch (err) { + report(err); + } + } +} + +export const productTracking = new ProductTracking(); diff --git a/packages/shared/lib/utils/utils.ts b/packages/shared/lib/utils/utils.ts index e029c85d327..3090adcca47 100644 --- a/packages/shared/lib/utils/utils.ts +++ b/packages/shared/lib/utils/utils.ts @@ -7,12 +7,6 @@ import { cloudHost, isEnterprise, isProd, isStaging, localhostUrl, stagingHost } import type { DBConnection, Provider } from '@nangohq/types'; -export enum UserType { - Local = 'localhost', - SelfHosted = 'self-hosted', - Cloud = 'cloud' -} - export enum NodeEnv { Dev = 'development', Staging = 'staging', diff --git a/packages/utils/lib/environment/parse.ts b/packages/utils/lib/environment/parse.ts index a9ec5b6888d..2817c1d0637 100644 --- a/packages/utils/lib/environment/parse.ts +++ b/packages/utils/lib/environment/parse.ts @@ -225,7 +225,6 @@ export const ENVS = z.object({ NANGO_TELEMETRY_SDK: bool, NANGO_ADMIN_KEY: z.string().optional(), NANGO_INTEGRATIONS_FULL_PATH: z.string().optional(), - TELEMETRY: bool, LOG_LEVEL: z.enum(['info', 'debug', 'warn', 'error']).optional().default('info') }); diff --git a/tests/setup.ts b/tests/setup.ts index db5d6c9643f..b87b97fff96 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -52,7 +52,6 @@ async function setupPostgres() { process.env['NANGO_DB_USER'] = user; process.env['NANGO_DB_PORT'] = port.toString(); process.env['NANGO_DB_NAME'] = dbName; - process.env['TELEMETRY'] = 'false'; process.env['RECORDS_DATABASE_URL'] = `postgres://${user}:${password}@localhost:${port}/${dbName}`; }