Skip to content

Commit

Permalink
feat: 🔥 integration with mixpanel
Browse files Browse the repository at this point in the history
integration with mixpanel
  • Loading branch information
Tal Rofe committed Jun 21, 2022
1 parent 66efddd commit 7c5c0c4
Show file tree
Hide file tree
Showing 56 changed files with 7,927 additions and 2,975 deletions.
1 change: 1 addition & 0 deletions apps/backend/@types/global/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ declare global {
readonly GITHUB_OAUTH_CLIENT_ID: string;
readonly GITHUB_OAUTH_CLIENT_SECRET: string;
readonly GITHUB_OAUTH_REDIRECT_URI: string;
readonly MIXPANEL_TOKEN: string;
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/envs/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ GOOGLE_OAUTH_CLIENT_SECRET="DUMMY"
GOOGLE_OAUTH_REDIRECT_URI="http://localhost:3000/user/auth/google-redirect"
GITHUB_OAUTH_CLIENT_ID="DUMMY"
GITHUB_OAUTH_CLIENT_SECRET="DUMMY"
GITHUB_OAUTH_REDIRECT_URI="http://localhost:3000/user/auth/github-redirect"
GITHUB_OAUTH_REDIRECT_URI="http://localhost:3000/user/auth/github-redirect"
MIXPANEL_TOKEN="XOMBILLAH"
4 changes: 3 additions & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"class-transformer": "0.5.1",
"class-validator": "0.13.2",
"googleapis": "103.0.0",
"mixpanel": "0.16.0",
"nestjs-real-ip": "2.1.1",
"passport": "0.6.0",
"passport-github2": "0.1.12",
"passport-google-oauth20": "2.0.0",
Expand Down Expand Up @@ -58,8 +60,8 @@
"eslint-plugin-node": "11.1.0",
"eslint-plugin-unused-imports": "2.0.0",
"open-cli": "7.0.1",
"rimraf": "3.0.2",
"prisma": "3.15.2",
"rimraf": "3.0.2",
"swagger-ui-express": "4.4.0",
"ts-loader": "9.3.0",
"typescript": "4.7.4",
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const EnvConfiguration = (): IEnvironment => ({
githubOAuthClientId: process.env.GITHUB_OAUTH_CLIENT_ID,
githubOAuthClientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
githubOAuthRedirectUri: process.env.GITHUB_OAUTH_REDIRECT_URI,
mixpanelToken: process.env.MIXPANEL_TOKEN,
});

export default EnvConfiguration;
1 change: 1 addition & 0 deletions apps/backend/src/config/env.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export interface IEnvironment {
readonly githubOAuthClientId: string;
readonly githubOAuthClientSecret: string;
readonly githubOAuthRedirectUri: string;
readonly mixpanelToken: string;
}
3 changes: 3 additions & 0 deletions apps/backend/src/config/env.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class EnvironmentVariables {

@IsString()
public GITHUB_OAUTH_REDIRECT_URI!: string;

@IsString()
public MIXPANEL_TOKEN!: string;
}

export const validate = (config: Record<string, unknown>) => {
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/modules/user/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { GithubAuthGuard } from './guards/github-auth.guard';
import { GithubStrategy } from './strategies/github.strategy';
import { GithubController } from './github.controller';
import { DeleteController } from './delete.controller';
import { EventHandlers } from './events/handlers';

@Module({
imports: [CqrsModule, PassportModule, JwtModule.register({})],
Expand All @@ -33,6 +34,7 @@ import { DeleteController } from './delete.controller';
providers: [
...QueryHandlers,
...CommandHandlers,
...EventHandlers,
AuthService,
LocalStrategy,
RefreshTokenStrategy,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class LoginMixpanelContract {
constructor(public readonly userId: string, public readonly ip: string) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class SignupMixpanelContract {
constructor(public readonly userId: string, public readonly ip: string) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { LoginMixpanelHandler } from './login-mixpanel.handler';
import { SignupMixpanelHandler } from './signup-mixpanel.handler';

export const EventHandlers = [SignupMixpanelHandler, LoginMixpanelHandler];
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ConfigService } from '@nestjs/config';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import Mixpanel from 'mixpanel';

import { IEnvironment } from '@/config/env.interface';

import { USER_LOGIN } from '../../models/mixpanel-events';
import { LoginMixpanelContract } from '../contracts/login-mixpanel.contract';

@CommandHandler(LoginMixpanelContract)
export class LoginMixpanelHandler implements ICommandHandler<LoginMixpanelContract> {
constructor(private readonly configService: ConfigService<IEnvironment, true>) {}

execute(contract: LoginMixpanelContract): Promise<void> {
const mixpanel = Mixpanel.init(this.configService.get('mixpanelToken', { infer: true }));

mixpanel.track(USER_LOGIN, {
distinct_id: contract.userId,
ip: contract.ip,
});

return Promise.resolve();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ConfigService } from '@nestjs/config';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import Mixpanel from 'mixpanel';

import { IEnvironment } from '@/config/env.interface';

import { SignupMixpanelContract } from '../contracts/signup-mixpanel.contract';
import { USER_SIGNUP } from '../../models/mixpanel-events';

@CommandHandler(SignupMixpanelContract)
export class SignupMixpanelHandler implements ICommandHandler<SignupMixpanelContract> {
constructor(private readonly configService: ConfigService<IEnvironment, true>) {}

execute(contract: SignupMixpanelContract): Promise<void> {
const mixpanel = Mixpanel.init(this.configService.get('mixpanelToken', { infer: true }));

mixpanel.track(USER_SIGNUP, {
distinct_id: contract.userId,
ip: contract.ip,
});

return Promise.resolve();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
Logger,
UseGuards,
} from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { CommandBus, EventBus, QueryBus } from '@nestjs/cqrs';
import { RealIP } from 'nestjs-real-ip';

import { Public } from '@/decorators/public.decorator';
import { ExternalAuthUser } from '@/decorators/external-auth-user.decorator';
Expand All @@ -23,6 +24,7 @@ import { UpdateExternalTokenContract } from './commands/contracts/update-externa
import Routes from './auth.routes';
import { IExternalAuthUser } from './interfaces/external-auth-user';
import { IExternalLoggedUser } from './interfaces/user';
import { LoginMixpanelContract } from './events/contracts/login-mixpanel.contract';

@Controller(Routes.CONTROLLER)
export class GithubController {
Expand All @@ -31,6 +33,7 @@ export class GithubController {
constructor(
private readonly queryBus: QueryBus,
private readonly commandBus: CommandBus,
private readonly eventBus: EventBus,
private readonly authService: AuthService,
) {}

Expand All @@ -47,6 +50,7 @@ export class GithubController {
@HttpCode(HttpStatus.OK)
public async githubRedirect(
@ExternalAuthUser() user: IExternalAuthUser,
@RealIP() ip: string,
): Promise<IGithubRedirectResponse> {
this.logger.log(
`User with an email "${user.email}" tries to login. Will check if already exists in DB`,
Expand All @@ -61,6 +65,7 @@ export class GithubController {

const createdGithubUserId = await this.queryBus.execute<CreateGithubUserContract, string>(
new CreateGithubUserContract({
ip,
name: user.name,
email: user.email,
accessToken: user.externalToken!,
Expand Down Expand Up @@ -127,6 +132,8 @@ export class GithubController {

this.logger.log('Successfully stored new refresh token and remove old ones');

this.eventBus.publish(new LoginMixpanelContract(githubUser.id, ip));

return {
accessToken,
refreshToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
Logger,
UseGuards,
} from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { CommandBus, EventBus, QueryBus } from '@nestjs/cqrs';
import { RealIP } from 'nestjs-real-ip';

import { Public } from '@/decorators/public.decorator';
import { ExternalAuthUser } from '@/decorators/external-auth-user.decorator';
Expand All @@ -23,6 +24,7 @@ import { UpdateExternalTokenContract } from './commands/contracts/update-externa
import Routes from './auth.routes';
import { IExternalAuthUser } from './interfaces/external-auth-user';
import { IExternalLoggedUser } from './interfaces/user';
import { LoginMixpanelContract } from './events/contracts/login-mixpanel.contract';

@Controller(Routes.CONTROLLER)
export class GoogleController {
Expand All @@ -31,6 +33,7 @@ export class GoogleController {
constructor(
private readonly queryBus: QueryBus,
private readonly commandBus: CommandBus,
private readonly eventBus: EventBus,
private readonly authService: AuthService,
) {}

Expand All @@ -47,6 +50,7 @@ export class GoogleController {
@HttpCode(HttpStatus.OK)
public async googleRedirect(
@ExternalAuthUser() user: IExternalAuthUser,
@RealIP() ip: string,
): Promise<IGoogleRedirectResponse> {
this.logger.log(
`User with an email "${user.email}" tries to login. Will check if already exists in DB`,
Expand All @@ -69,6 +73,7 @@ export class GoogleController {

const createdGoogleUserId = await this.queryBus.execute<CreateGoogleUserContract, string>(
new CreateGoogleUserContract({
ip,
name: user.name,
email: user.email,
refreshToken: user.externalToken!,
Expand Down Expand Up @@ -135,6 +140,8 @@ export class GoogleController {

this.logger.log('Successfully stored new refresh token and remove old ones');

this.eventBus.publish(new LoginMixpanelContract(googleUser.id, ip));

return {
accessToken,
refreshToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const USER_LOGIN = 'User_Login';

export const USER_SIGNUP = 'User_Signup';
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
interface ICreateGithubUserData {
readonly ip: string;
readonly name: string;
readonly email: string;
readonly accessToken: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
interface ICreateGoogleUserData {
readonly ip: string;
readonly name: string;
readonly email: string;
readonly refreshToken: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { EventBus, IQueryHandler, QueryHandler } from '@nestjs/cqrs';

import { DBUserService } from '@/modules/database/user.service';

import { CreateGithubUserContract } from '../contracts/create-github-user.contract';
import { SignupMixpanelContract } from '../../events/contracts/signup-mixpanel.contract';

@QueryHandler(CreateGithubUserContract)
export class CreateGithubUserHandler implements IQueryHandler<CreateGithubUserContract> {
constructor(private readonly dbUserService: DBUserService) {}
constructor(private readonly dbUserService: DBUserService, private readonly eventBus: EventBus) {}

async execute(contract: CreateGithubUserContract) {
const createdUser = await this.dbUserService.createUser({
Expand All @@ -16,6 +17,8 @@ export class CreateGithubUserHandler implements IQueryHandler<CreateGithubUserCo
externalToken: contract.data.accessToken,
});

this.eventBus.publish(new SignupMixpanelContract(createdUser.id, contract.data.ip));

return createdUser.id;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { EventBus, IQueryHandler, QueryHandler } from '@nestjs/cqrs';

import { DBUserService } from '@/modules/database/user.service';

import { CreateGoogleUserContract } from '../contracts/create-google-user.contract';
import { SignupMixpanelContract } from '../../events/contracts/signup-mixpanel.contract';

@QueryHandler(CreateGoogleUserContract)
export class CreateGoogleUserHandler implements IQueryHandler<CreateGoogleUserContract> {
constructor(private readonly dbUserService: DBUserService) {}
constructor(private readonly dbUserService: DBUserService, private readonly eventBus: EventBus) {}

async execute(contract: CreateGoogleUserContract) {
const createdUser = await this.dbUserService.createUser({
Expand All @@ -16,6 +17,8 @@ export class CreateGoogleUserHandler implements IQueryHandler<CreateGoogleUserCo
authType: 'GOOGLE',
});

this.eventBus.publish(new SignupMixpanelContract(createdUser.id, contract.data.ip));

return createdUser.id;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Controller, Logger, Post } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { RealIP } from 'nestjs-real-ip';

import { CurrentUserId } from '@/decorators/current-user-id.decorator';

Expand All @@ -14,11 +15,11 @@ export class CreateController {
constructor(private readonly queryBus: QueryBus) {}

@Post(Routes.CREATE)
public async create(@CurrentUserId() userId: string): Promise<ICreateGroup> {
public async create(@CurrentUserId() userId: string, @RealIP() ip: string): Promise<ICreateGroup> {
this.logger.log(`Will try to create a group for a user with an Id: ${userId}`);

const createdGroupId = await this.queryBus.execute<CreateGroupContract, string>(
new CreateGroupContract(userId),
new CreateGroupContract(userId, ip),
);

this.logger.log(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class CreateGroupMixpanelContract {
constructor(public readonly userId: string, public readonly ip: string) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ConfigService } from '@nestjs/config';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import Mixpanel from 'mixpanel';

import { IEnvironment } from '@/config/env.interface';

import { CreateGroupMixpanelContract } from '../contracts/create-group-mixpanel.contract';
import { GROUP_CREATE } from '../../models/mixpanel-events';

@CommandHandler(CreateGroupMixpanelContract)
export class CreateGroupMixpanelHandler implements ICommandHandler<CreateGroupMixpanelContract> {
constructor(private readonly configService: ConfigService<IEnvironment, true>) {}

execute(contract: CreateGroupMixpanelContract): Promise<void> {
const mixpanel = Mixpanel.init(this.configService.get('mixpanelToken', { infer: true }));

mixpanel.track(GROUP_CREATE, {
distinct_id: contract.userId,
ip: contract.ip,
});

return Promise.resolve();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CreateGroupMixpanelHandler } from './create-group-mixpanel.handler';

export const EventHandlers = [CreateGroupMixpanelHandler];
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import { CreateController } from './create.controller';
import { DeleteController } from './delete.controller';
import { EditLabelController } from './edit-label.controller';
import { QueryHandlers } from './queries/handlers';
import { EventHandlers } from './events/handlers';

@Module({
imports: [CqrsModule],
controllers: [CreateController, EditLabelController, DeleteController],
providers: [...QueryHandlers, ...CommandHandlers, BelongingGroupGuard],
providers: [...QueryHandlers, ...CommandHandlers, ...EventHandlers, BelongingGroupGuard],
})
export class GroupsModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const GROUP_CREATE = 'Group_Create';
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export class CreateGroupContract {
constructor(public readonly userId: string) {}
constructor(public readonly userId: string, public readonly ip: string) {}
}
Loading

0 comments on commit 7c5c0c4

Please sign in to comment.