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