feat: SH instance analytics data collection (#3838)
This commit is contained in:
@@ -34,12 +34,14 @@
|
|||||||
"@nestjs/jwt": "^10.1.1",
|
"@nestjs/jwt": "^10.1.1",
|
||||||
"@nestjs/passport": "^10.0.2",
|
"@nestjs/passport": "^10.0.2",
|
||||||
"@nestjs/platform-express": "^10.2.6",
|
"@nestjs/platform-express": "^10.2.6",
|
||||||
|
"@nestjs/schedule": "^4.0.1",
|
||||||
"@nestjs/throttler": "^5.0.0",
|
"@nestjs/throttler": "^5.0.0",
|
||||||
"@prisma/client": "^5.8.0",
|
"@prisma/client": "^5.8.0",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
|
"cron": "^3.1.6",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"fp-ts": "^2.13.1",
|
"fp-ts": "^2.13.1",
|
||||||
@@ -57,6 +59,7 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"passport-microsoft": "^1.0.0",
|
"passport-microsoft": "^1.0.0",
|
||||||
|
"posthog-node": "^3.6.3",
|
||||||
"prisma": "^5.8.0",
|
"prisma": "^5.8.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
InfraConfigArgs,
|
InfraConfigArgs,
|
||||||
} from 'src/infra-config/input-args';
|
} from 'src/infra-config/input-args';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||||
|
import { ServiceStatus } from 'src/infra-config/helper';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => Infra)
|
@Resolver(() => Infra)
|
||||||
@@ -310,6 +311,25 @@ export class InfraResolver {
|
|||||||
return updatedRes.right;
|
return updatedRes.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Boolean, {
|
||||||
|
description: 'Enable or disable analytics collection',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async toggleAnalyticsCollection(
|
||||||
|
@Args({
|
||||||
|
name: 'status',
|
||||||
|
type: () => ServiceStatus,
|
||||||
|
description: 'Toggle analytics collection',
|
||||||
|
})
|
||||||
|
analyticsCollectionStatus: ServiceStatus,
|
||||||
|
) {
|
||||||
|
const res = await this.infraConfigService.toggleAnalyticsCollection(
|
||||||
|
analyticsCollectionStatus,
|
||||||
|
);
|
||||||
|
if (E.isLeft(res)) throwErr(res.left);
|
||||||
|
return res.right;
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
description: 'Reset Infra Configs with default values (.env)',
|
description: 'Reset Infra Configs with default values (.env)',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
|||||||
import { InfraConfigModule } from './infra-config/infra-config.module';
|
import { InfraConfigModule } from './infra-config/infra-config.module';
|
||||||
import { loadInfraConfiguration } from './infra-config/helper';
|
import { loadInfraConfiguration } from './infra-config/helper';
|
||||||
import { MailerModule } from './mailer/mailer.module';
|
import { MailerModule } from './mailer/mailer.module';
|
||||||
|
import { PosthogModule } from './posthog/posthog.module';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -96,6 +98,8 @@ import { MailerModule } from './mailer/mailer.module';
|
|||||||
UserCollectionModule,
|
UserCollectionModule,
|
||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
InfraConfigModule,
|
InfraConfigModule,
|
||||||
|
PosthogModule,
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
],
|
],
|
||||||
providers: [GQLComplexityPlugin],
|
providers: [GQLComplexityPlugin],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@@ -711,3 +711,9 @@ export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
|||||||
*/
|
*/
|
||||||
export const DATABASE_TABLE_NOT_EXIST =
|
export const DATABASE_TABLE_NOT_EXIST =
|
||||||
'Database migration not found. Please check the documentation for assistance: https://docs.hoppscotch.io/documentation/self-host/community-edition/install-and-build#running-migrations';
|
'Database migration not found. Please check the documentation for assistance: https://docs.hoppscotch.io/documentation/self-host/community-edition/install-and-build#running-migrations';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PostHog client is not initialized
|
||||||
|
* (InfraConfigService)
|
||||||
|
*/
|
||||||
|
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AUTH_PROVIDER_NOT_CONFIGURED } from 'src/errors';
|
|||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
export enum ServiceStatus {
|
export enum ServiceStatus {
|
||||||
ENABLE = 'ENABLE',
|
ENABLE = 'ENABLE',
|
||||||
@@ -104,3 +105,12 @@ export function getConfiguredSSOProviders() {
|
|||||||
|
|
||||||
return configuredAuthProviders.join(',');
|
return configuredAuthProviders.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a hashed valued for analytics
|
||||||
|
* @returns Generated hashed value
|
||||||
|
*/
|
||||||
|
export function generateAnalyticsUserId() {
|
||||||
|
const hashedUserID = randomBytes(20).toString('hex');
|
||||||
|
return hashedUserID;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ import {
|
|||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { ServiceStatus, getConfiguredSSOProviders, stopApp } from './helper';
|
import {
|
||||||
|
ServiceStatus,
|
||||||
|
generateAnalyticsUserId,
|
||||||
|
getConfiguredSSOProviders,
|
||||||
|
stopApp,
|
||||||
|
} from './helper';
|
||||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
|
|
||||||
@@ -75,6 +80,14 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
value: getConfiguredSSOProviders(),
|
value: getConfiguredSSOProviders(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
|
value: false.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
|
value: generateAnalyticsUserId(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
value: (await this.prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
value: (await this.prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
||||||
@@ -231,6 +244,22 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or Disable Analytics Collection
|
||||||
|
*
|
||||||
|
* @param status Status to enable or disable
|
||||||
|
* @returns Boolean of status of analytics collection
|
||||||
|
*/
|
||||||
|
async toggleAnalyticsCollection(status: ServiceStatus) {
|
||||||
|
const isUpdated = await this.update(
|
||||||
|
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
|
status === ServiceStatus.ENABLE ? 'true' : 'false',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||||
|
return E.right(isUpdated.right.value === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or Disable SSO for login/signup
|
* Enable or Disable SSO for login/signup
|
||||||
* @param provider Auth Provider to enable or disable
|
* @param provider Auth Provider to enable or disable
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PosthogService } from './posthog.service';
|
||||||
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [PosthogService],
|
||||||
|
})
|
||||||
|
export class PosthogModule {}
|
||||||
58
packages/hoppscotch-backend/src/posthog/posthog.service.ts
Normal file
58
packages/hoppscotch-backend/src/posthog/posthog.service.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PostHog } from 'posthog-node';
|
||||||
|
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import { CronJob } from 'cron';
|
||||||
|
import { POSTHOG_CLIENT_NOT_INITIALIZED } from 'src/errors';
|
||||||
|
import { throwErr } from 'src/utils';
|
||||||
|
@Injectable()
|
||||||
|
export class PosthogService {
|
||||||
|
private postHogClient: PostHog;
|
||||||
|
private POSTHOG_API_KEY = 'phc_9CipPajQC22mSkk2wxe2TXsUA0Ysyupe8dt5KQQELqx';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private schedulerRegistry: SchedulerRegistry,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
if (this.configService.get('INFRA.ALLOW_ANALYTICS_COLLECTION') === 'true') {
|
||||||
|
console.log('Initializing PostHog');
|
||||||
|
this.postHogClient = new PostHog(this.POSTHOG_API_KEY, {
|
||||||
|
host: 'https://eu.posthog.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule the cron job only if analytics collection is allowed
|
||||||
|
this.scheduleCronJob();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleCronJob() {
|
||||||
|
const job = new CronJob(CronExpression.EVERY_WEEK, async () => {
|
||||||
|
await this.capture();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.schedulerRegistry.addCronJob('captureAnalytics', job);
|
||||||
|
job.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async capture() {
|
||||||
|
if (!this.postHogClient) {
|
||||||
|
throwErr(POSTHOG_CLIENT_NOT_INITIALIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.postHogClient.capture({
|
||||||
|
distinctId: this.configService.get('INFRA.ANALYTICS_USER_ID'),
|
||||||
|
event: 'sh_instance',
|
||||||
|
properties: {
|
||||||
|
type: 'COMMUNITY',
|
||||||
|
total_user_count: await this.prismaService.user.count(),
|
||||||
|
total_workspace_count: await this.prismaService.team.count(),
|
||||||
|
version: this.configService.get('npm_package_version'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('Sent event to PostHog');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ export enum InfraConfigEnum {
|
|||||||
|
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
||||||
|
|
||||||
|
ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION',
|
||||||
|
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
|
||||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,5 +31,6 @@ export enum InfraConfigEnumForClient {
|
|||||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||||
|
|
||||||
|
ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION',
|
||||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||||
}
|
}
|
||||||
|
|||||||
13902
pnpm-lock.yaml
generated
13902
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user