From f118e90a0c777621ddf28b1bced86a278a420d34 Mon Sep 17 00:00:00 2001 From: Abhijeet <129729795+luciferlinx101@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:14:50 +0530 Subject: [PATCH] Feature first login src (#1241) Adding source in user database for analytics. --- gui/pages/_app.js | 69 ++++++++++++------- gui/pages/api/DashboardService.js | 6 +- gui/utils/utils.js | 18 ++++- .../3867bb00a495_added_first_login_source.py | 28 ++++++++ superagi/controllers/user.py | 18 ++++- superagi/models/user.py | 3 +- tests/unit_tests/controllers/test_user.py | 37 ++++++++++ 7 files changed, 152 insertions(+), 27 deletions(-) create mode 100644 migrations/versions/3867bb00a495_added_first_login_source.py create mode 100644 tests/unit_tests/controllers/test_user.py diff --git a/gui/pages/_app.js b/gui/pages/_app.js index 6fe6ec6a4..7a3bb14c6 100644 --- a/gui/pages/_app.js +++ b/gui/pages/_app.js @@ -14,7 +14,7 @@ import { validateAccessToken, checkEnvironment, addUser, - installToolkitTemplate, installAgentTemplate, installKnowledgeTemplate + installToolkitTemplate, installAgentTemplate, installKnowledgeTemplate, getFirstSignup } from "@/pages/api/DashboardService"; import {githubClientId} from "@/pages/api/apiConfig"; import { @@ -22,7 +22,7 @@ import { } from "@/pages/api/DashboardService"; import {useRouter} from 'next/router'; import querystring from 'querystring'; -import {refreshUrl, loadingTextEffect} from "@/utils/utils"; +import {refreshUrl, loadingTextEffect, getUTMParametersFromURL, setLocalStorageValue} from "@/utils/utils"; import MarketplacePublic from "./Content/Marketplace/MarketplacePublic" import {toast} from "react-toastify"; @@ -101,12 +101,7 @@ export default function App() { } useEffect(() => { - if (window.location.href.toLowerCase().includes('marketplace')) { - setShowMarketplace(true); - } else { - installFromMarketplace(); - } - + handleMarketplace() loadingTextEffect('Initializing SuperAGI', setLoadingText, 500); checkEnvironment() @@ -124,34 +119,28 @@ export default function App() { const parsedParams = querystring.parse(queryParams); let access_token = parsedParams.access_token || null; + const utmParams = getUTMParametersFromURL(); + if (utmParams) + sessionStorage.setItem('utm_source', utmParams.utm_source); + const signupSource = sessionStorage.getItem('utm_source'); + if (typeof window !== 'undefined' && access_token) { localStorage.setItem('accessToken', access_token); refreshUrl(); } - validateAccessToken() .then((response) => { setUserName(response.data.name || ''); + if(signupSource) { + handleSignUpSource(signupSource) + } fetchOrganisation(response.data.id); }) .catch((error) => { console.error('Error validating access token:', error); }); } else { - const userData = { - "name": "SuperAGI User", - "email": "super6@agi.com", - "password": "pass@123", - } - - addUser(userData) - .then((response) => { - setUserName(response.data.name); - fetchOrganisation(response.data.id); - }) - .catch((error) => { - console.error('Error adding user:', error); - }); + handleLocalEnviroment() } }) .catch((error) => { @@ -197,6 +186,40 @@ export default function App() { } } + const handleLocalEnviroment = () => { + const userData = { + "name": "SuperAGI User", + "email": "super6@agi.com", + "password": "pass@123", + } + + addUser(userData) + .then((response) => { + setUserName(response.data.name); + fetchOrganisation(response.data.id); + }) + .catch((error) => { + console.error('Error adding user:', error); + }); + }; + const handleSignUpSource = (signup) => { + getFirstSignup(signup) + .then((response) => { + sessionStorage.removeItem('utm_source'); + }) + .catch((error) => { + console.error('Error validating source:', error); + }) + }; + + const handleMarketplace = () => { + if (window.location.href.toLowerCase().includes('marketplace')) { + setShowMarketplace(true); + } else { + installFromMarketplace(); + } + }; + useEffect(() => { const clearLocalStorage = () => { Object.keys(localStorage).forEach((key) => { diff --git a/gui/pages/api/DashboardService.js b/gui/pages/api/DashboardService.js index c26330d25..67df3c475 100644 --- a/gui/pages/api/DashboardService.js +++ b/gui/pages/api/DashboardService.js @@ -395,4 +395,8 @@ export const getKnowledgeMetrics = (knowledgeName) => { export const getKnowledgeLogs = (knowledgeName) => { return api.get(`analytics/knowledge/${knowledgeName}/logs`) -} \ No newline at end of file +} + +export const getFirstSignup = (source) => { + return api.post(`/users/first_login_source/${source}`,); +}; \ No newline at end of file diff --git a/gui/utils/utils.js b/gui/utils/utils.js index 7ef7015ce..ca516eb15 100644 --- a/gui/utils/utils.js +++ b/gui/utils/utils.js @@ -542,4 +542,20 @@ export const convertWaitingPeriod = (waitingPeriod) => { // hour: 'numeric', // minute: 'numeric' // }); -// } \ No newline at end of file +// } + +export const getUTMParametersFromURL = () => { + const params = new URLSearchParams(window.location.search); + + const utmParams = { + utm_source: params.get('utm_source') || '', + utm_medium: params.get('utm_medium') || '', + utm_campaign: params.get('utm_campaign') || '', + }; + + if (!utmParams.utm_source && !utmParams.utm_medium && !utmParams.utm_campaign) { + return null; + } + + return utmParams; +} \ No newline at end of file diff --git a/migrations/versions/3867bb00a495_added_first_login_source.py b/migrations/versions/3867bb00a495_added_first_login_source.py new file mode 100644 index 000000000..eaf28f0ef --- /dev/null +++ b/migrations/versions/3867bb00a495_added_first_login_source.py @@ -0,0 +1,28 @@ +"""added_first_login_source + +Revision ID: 3867bb00a495 +Revises: 661ec8a4c32e +Create Date: 2023-09-15 02:06:24.006555 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3867bb00a495' +down_revision = '661ec8a4c32e' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('first_login_source', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'first_login_source') + # ### end Alembic commands ### diff --git a/superagi/controllers/user.py b/superagi/controllers/user.py index 19bf2c935..c550fd889 100644 --- a/superagi/controllers/user.py +++ b/superagi/controllers/user.py @@ -11,12 +11,14 @@ from superagi.models.user import User from fastapi import APIRouter -from superagi.helper.auth import check_auth +from superagi.helper.auth import check_auth, get_current_user from superagi.lib.logger import logger + # from superagi.types.db import UserBase, UserIn, UserOut router = APIRouter() + class UserBase(BaseModel): name: str email: str @@ -42,6 +44,7 @@ class UserIn(UserBase): class Config: orm_mode = True + # CRUD Operations @router.post("/add", response_model=UserOut, status_code=201) def create_user(user: UserIn, @@ -126,3 +129,16 @@ def update_user(user_id: int, db.session.commit() return db_user + + +@router.post("/first_login_source/{source}") +def update_first_login_source(source: str, Authorize: AuthJWT = Depends(check_auth)): + """ Update first login source of the user """ + user = get_current_user(Authorize) + # valid_sources = ['google', 'github', 'email'] + if user.first_login_source is None or user.first_login_source == '': + user.first_login_source = source + db.session.commit() + db.session.flush() + logger.info("User : ",user) + return user diff --git a/superagi/models/user.py b/superagi/models/user.py index 519095756..6ef8bb694 100644 --- a/superagi/models/user.py +++ b/superagi/models/user.py @@ -24,6 +24,7 @@ class User(DBBaseModel): email = Column(String, unique=True) password = Column(String) organisation_id = Column(Integer) + first_login_source = Column(String) def __repr__(self): """ @@ -34,4 +35,4 @@ def __repr__(self): """ return f"User(id={self.id}, name='{self.name}', email='{self.email}', password='{self.password}'," \ - f"organisation_id={self.organisation_id})" + f"organisation_id={self.organisation_id}, first_login_source={self.first_login_source})" diff --git a/tests/unit_tests/controllers/test_user.py b/tests/unit_tests/controllers/test_user.py new file mode 100644 index 000000000..0ab124dd7 --- /dev/null +++ b/tests/unit_tests/controllers/test_user.py @@ -0,0 +1,37 @@ +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient + +from main import app +from superagi.models.user import User + +client = TestClient(app) + +# Define a fixture for an authenticated user +@pytest.fixture +def authenticated_user(): + # Create a mock user object with necessary attributes + user = User() + + # Set user attributes + user.id = 1 # User ID + user.username = "testuser" # User's username + user.email = "super6@agi.com" # User's email + user.first_login_source = None # User's first login source + user.token = "mock-jwt-token" + + return user + +# Test case for updating first login source when it's not set +def test_update_first_login_source(authenticated_user): + with patch('superagi.helper.auth.db') as mock_auth_db: + source = "github" # Specify the source you want to set + + mock_auth_db.session.query.return_value.filter.return_value.first.return_value = authenticated_user + response = client.post(f"users/first_login_source/{source}", headers={"Authorization": f"Bearer {authenticated_user.token}"}) + + # Verify the HTTP response + assert response.status_code == 200 + assert "first_login_source" in response.json() # Check if the "first_login_source" field is in the response + assert response.json()["first_login_source"] == "github" # Check if the "source" field equals "github" \ No newline at end of file