diff --git a/apps/api/package.json b/apps/api/package.json index bd118d152..d65b8e296 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -19,6 +19,7 @@ "test": "cross-env TZ=UTC NODE_ENV=test E2E_RUNNER=true mocha --timeout 10000 --require ts-node/register --exit" }, "dependencies": { + "@amplitude/analytics-node": "^1.3.6", "@impler/client": "workspace:^", "@impler/dal": "workspace:^", "@impler/services": "workspace:^", diff --git a/apps/api/src/app/review/review.module.ts b/apps/api/src/app/review/review.module.ts index 29571acea..26b851ccd 100644 --- a/apps/api/src/app/review/review.module.ts +++ b/apps/api/src/app/review/review.module.ts @@ -5,10 +5,11 @@ import { SharedModule } from '@shared/shared.module'; import { AJVService } from './service/AJV.service'; import { Sandbox, SManager } from '../shared/services/sandbox'; import { QueueService } from '@shared/services/queue.service'; +import { AmplitudeService } from '@shared/services/amplitude.service'; @Module({ imports: [SharedModule], - providers: [...USE_CASES, AJVService, QueueService, SManager, Sandbox], + providers: [...USE_CASES, AJVService, QueueService, SManager, Sandbox, AmplitudeService], controllers: [ReviewController], }) export class ReviewModule {} diff --git a/apps/api/src/app/review/usecases/start-process/start-process.usecase.ts b/apps/api/src/app/review/usecases/start-process/start-process.usecase.ts index d72a743eb..00cbcfd51 100644 --- a/apps/api/src/app/review/usecases/start-process/start-process.usecase.ts +++ b/apps/api/src/app/review/usecases/start-process/start-process.usecase.ts @@ -10,6 +10,7 @@ import { } from '@impler/shared'; import { PaymentAPIService } from '@impler/services'; import { QueueService } from '@shared/services/queue.service'; +import { AmplitudeService } from '@shared/services/amplitude.service'; import { DalService, TemplateEntity, UploadRepository } from '@impler/dal'; @Injectable() @@ -18,6 +19,7 @@ export class StartProcess { private dalService: DalService, private queueService: QueueService, private uploadRepository: UploadRepository, + private amplitudeService: AmplitudeService, private paymentAPIService: PaymentAPIService ) {} @@ -58,6 +60,12 @@ export class StartProcess { ); } + this.amplitudeService.recordsImported(userEmail, { + records: uploadInfo.totalRecords, + valid: uploadInfo.validRecords, + invalid: uploadInfo.invalidRecords, + }); + this.queueService.publishToQueue(QueuesEnum.END_IMPORT, { uploadId: _uploadId, destination: destination, diff --git a/apps/api/src/app/shared/services/amplitude.service.ts b/apps/api/src/app/shared/services/amplitude.service.ts new file mode 100644 index 000000000..ab0ab2c41 --- /dev/null +++ b/apps/api/src/app/shared/services/amplitude.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { init, track } from '@amplitude/analytics-node'; + +@Injectable() +export class AmplitudeService { + constructor() { + if (process.env.AMPLITUDE_ID) { + init(process.env.AMPLITUDE_ID); + } + } + recordsImported(email: string, data: { records: number; valid: number; invalid: number }) { + if (process.env.AMPLITUDE_ID) { + track('RECORDS IMPORTED', data, { + user_id: email, + }); + } + } +} diff --git a/apps/api/src/types/env.d.ts b/apps/api/src/types/env.d.ts index d1f775186..028fac788 100644 --- a/apps/api/src/types/env.d.ts +++ b/apps/api/src/types/env.d.ts @@ -42,5 +42,7 @@ declare namespace NodeJS { PAYMENT_API_BASE_URL: string; PAYMENT_AUTH_KEY: string; PAYMENT_AUTH_VALUE: string; + + AMPLITUDE_ID: string; } } diff --git a/apps/web/components/Integration/IntegrationModal.tsx b/apps/web/components/Integration/IntegrationModal.tsx index be05877cf..2b6de8fe1 100644 --- a/apps/web/components/Integration/IntegrationModal.tsx +++ b/apps/web/components/Integration/IntegrationModal.tsx @@ -1,7 +1,9 @@ import getConfig from 'next/config'; import { useEffect, useState } from 'react'; import { Flex, Title, useMantineColorScheme } from '@mantine/core'; + import { colors } from '@config'; +import { track } from '@libs/amplitude'; import { IntegrationEnum } from '@impler/shared'; import { IntegrationTabs } from './IntegrationTabs'; import { integrationData } from './IntegrationData'; @@ -27,6 +29,26 @@ export function IntegrationModal({ accessToken, projectId, templateId, integrati setSelectedTab(Object.keys(integrationData[integration])[0]); }, [integration]); + const onIntegrationFrameworkChange = (value: string) => { + track({ + name: 'CHANGE INTEGRATION FRAMEWORK', + properties: { + framework: value, + }, + }); + setIntegration(value as IntegrationEnum); + }; + + const onIntegrationStepChange = (newStep: string) => { + track({ + name: 'CHANGE INTEGRATION STEPS', + properties: { + step: newStep, + }, + }); + setSelectedTab(newStep); + }; + const tabs = Object.keys(integrationData[integration]); return ( @@ -35,12 +57,12 @@ export function IntegrationModal({ accessToken, projectId, templateId, integrati Integrate - + ({ id: tab.toLowerCase().replace(/\s+/g, ''), value: tab, diff --git a/apps/web/hooks/useImportDetails.tsx b/apps/web/hooks/useImportDetails.tsx index 37c7e83f0..01e61d7c1 100644 --- a/apps/web/hooks/useImportDetails.tsx +++ b/apps/web/hooks/useImportDetails.tsx @@ -83,6 +83,10 @@ export function useImportDetails({ templateId }: useImportDetailProps) { }; const onIntegrationClick = () => { + track({ + name: 'INTEGRATE', + properties: {}, + }); if (templateData && profileInfo) { modals.open({ modalId: MODAL_KEYS.INTEGRATION_DETAILS, diff --git a/apps/web/hooks/useImports.tsx b/apps/web/hooks/useImports.tsx index 2223a2404..9ac8f4aff 100644 --- a/apps/web/hooks/useImports.tsx +++ b/apps/web/hooks/useImports.tsx @@ -45,7 +45,9 @@ export function useImports() { ]); track({ name: 'IMPORT CREATE', - properties: {}, + properties: { + framework: data.integration, + }, }); push(`/imports/${data._id}`); notify(NOTIFICATION_KEYS.IMPORT_CREATED); diff --git a/apps/web/hooks/useOnboardUserProjectForm.tsx b/apps/web/hooks/useOnboardUserProjectForm.tsx index 4246b4352..190ecd5a2 100644 --- a/apps/web/hooks/useOnboardUserProjectForm.tsx +++ b/apps/web/hooks/useOnboardUserProjectForm.tsx @@ -20,7 +20,7 @@ export function useOnboardUserProjectForm() { [API_KEYS.ONBOARD_USER], (apiData) => commonApi(API_KEYS.ONBOARD_USER as any, { body: { ...apiData, onboarding: true } }), { - onSuccess: () => { + onSuccess: (_, onboardData) => { if (profileInfo) { setProfileInfo({ ...profileInfo, @@ -33,6 +33,14 @@ export function useOnboardUserProjectForm() { duringOnboard: true, }, }); + track({ + name: 'ONBOARD', + properties: { + companySize: onboardData.companySize, + role: onboardData.role, + source: onboardData.source, + }, + }); push('/'); }, } diff --git a/apps/web/hooks/useValidator.tsx b/apps/web/hooks/useValidator.tsx index 4444df7ed..4a46cacf9 100644 --- a/apps/web/hooks/useValidator.tsx +++ b/apps/web/hooks/useValidator.tsx @@ -5,6 +5,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { notify } from '@libs/notify'; import { commonApi } from '@libs/api'; +import { track } from '@libs/amplitude'; import { API_KEYS, MODAL_KEYS, MODAL_TITLES } from '@config'; import { ICustomization, IErrorObject, IValidator } from '@impler/shared'; import { CodeOutput } from '@components/imports/validator/CodeOutput'; @@ -87,6 +88,12 @@ export function useValidator({ templateId }: UseSchemaProps) { } else { notify('VALIDATIONS_UPDATED'); } + track({ + name: 'SAVE VALIDATION', + properties: { + isValid: !!output?.passed, + }, + }); }, onError(error) { notify('VALIDATIONS_UPDATED', { title: 'Something went wrong!', message: error?.message, color: 'red' }); diff --git a/apps/web/libs/amplitude.ts b/apps/web/libs/amplitude.ts index 5b2fb90d4..fc822ebe1 100644 --- a/apps/web/libs/amplitude.ts +++ b/apps/web/libs/amplitude.ts @@ -47,7 +47,9 @@ type TrackData = } | { name: 'IMPORT CREATE'; - properties: Record; + properties: { + framework: string; + }; } | { name: 'OUTPUT FORMAT UPDATED'; @@ -65,6 +67,14 @@ type TrackData = duringOnboard: boolean; }; } + | { + name: 'ONBOARD'; + properties: { + companySize: string; + role: string; + source: string; + }; + } | { name: 'PROJECT SWITCH'; properties: Record; @@ -151,6 +161,28 @@ type TrackData = properties: { yearly: boolean; }; + } + | { + name: 'SAVE VALIDATION'; + properties: { + isValid: boolean; + }; + } + | { + name: 'INTEGRATE'; + properties: Record; + } + | { + name: 'CHANGE INTEGRATION FRAMEWORK'; + properties: { + framework: string; + }; + } + | { + name: 'CHANGE INTEGRATION STEPS'; + properties: { + step: string; + }; }; export function track({ name, properties }: TrackData) { diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 2df90b55d..1d5ebe3dd 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -23,7 +23,11 @@ const { publicRuntimeConfig } = getConfig(); if (typeof window !== 'undefined' && publicRuntimeConfig.NEXT_PUBLIC_AMPLITUDE_ID) { init(publicRuntimeConfig.NEXT_PUBLIC_AMPLITUDE_ID, { defaultTracking: { + sessions: false, + pageViews: false, attribution: false, + fileDownloads: false, + formInteractions: false, }, }); } diff --git a/apps/web/pages/imports/[id].tsx b/apps/web/pages/imports/[id].tsx index 672314505..1fac00137 100644 --- a/apps/web/pages/imports/[id].tsx +++ b/apps/web/pages/imports/[id].tsx @@ -105,7 +105,7 @@ export default function ImportDetails({}) { > Import -