diff --git a/.gitignore b/.gitignore
index 6696a429b5..eab6e79ab7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,4 +62,4 @@ CLAUDE.md
.opencode/
.claude/
-storybook/yarn.lock
\ No newline at end of file
+storybook/yarn.lock
diff --git a/package.json b/package.json
index 06febdbc3e..be5f7bc84c 100644
--- a/package.json
+++ b/package.json
@@ -114,7 +114,7 @@
"eslint-plugin-xss": "^0.1.12",
"husky": "^8.0.0",
"jsdom": "^22.1.0",
- "postcss": "^8.4.31",
+ "postcss": "^8.5.8",
"prettier": "^3.0.0",
"sass": "1.77.6",
"start-server-and-test": "^2.0.0",
diff --git a/src/App.vue b/src/App.vue
index 7a6433ad72..b7d12f2963 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -6,10 +6,14 @@
import { useThemeStore, DARK_SCHEME_QUERY } from '@/stores/theme'
import { storeToRefs } from 'pinia'
import { themeApply } from '@/helpers'
+ import { captureFirstSessionUrl } from '@/helpers/first-session-url'
import Layout from '@/layout'
import '@modules/real-time-metrics/helpers/convert-date'
import '@/helpers/store-handler'
+ // Capture the first session URL as early as possible
+ captureFirstSessionUrl()
+
const DEFAULT_TITLE = 'Azion Console'
/** @type {import('@/plugins/analytics/AnalyticsTrackerAdapter').AnalyticsTrackerAdapter} */
diff --git a/src/helpers/first-session-url.js b/src/helpers/first-session-url.js
new file mode 100644
index 0000000000..126a201c4e
--- /dev/null
+++ b/src/helpers/first-session-url.js
@@ -0,0 +1,37 @@
+const FIRST_SESSION_URL_KEY = 'azion_first_session_url'
+
+/**
+ * Captures the first session URL and stores it in localStorage.
+ * This should be called early in the application lifecycle.
+ * The URL is only stored once per user session.
+ */
+export const captureFirstSessionUrl = () => {
+ if (typeof window === 'undefined') return null
+
+ // Only capture if not already stored
+ if (!localStorage.getItem(FIRST_SESSION_URL_KEY)) {
+ const currentUrl = window.location.href
+ localStorage.setItem(FIRST_SESSION_URL_KEY, currentUrl)
+ return currentUrl
+ }
+
+ return getFirstSessionUrl()
+}
+
+/**
+ * Retrieves the first session URL from localStorage.
+ * @returns {string|null} The first session URL or null if not set.
+ */
+export const getFirstSessionUrl = () => {
+ if (typeof window === 'undefined') return null
+ return localStorage.getItem(FIRST_SESSION_URL_KEY)
+}
+
+/**
+ * Clears the first session URL from localStorage.
+ * This can be called after the signup flow is complete.
+ */
+export const clearFirstSessionUrl = () => {
+ if (typeof window === 'undefined') return
+ localStorage.removeItem(FIRST_SESSION_URL_KEY)
+}
diff --git a/src/plugins/analytics/trackers/SignInTracker.js b/src/plugins/analytics/trackers/SignInTracker.js
index b2afb7a165..35cdb88349 100644
--- a/src/plugins/analytics/trackers/SignInTracker.js
+++ b/src/plugins/analytics/trackers/SignInTracker.js
@@ -1,3 +1,24 @@
+import { hubspotFormSubmitService } from '@/services/hubspot-services'
+
+/**
+ * Maps signin type flags to the appropriate form_action value for HubSpot.
+ * @param {Object} signupTypeFlags - The signin type flags
+ * @returns {string} The form_action value
+ */
+const FORM_ACTION_PRIORITY = [
+ 'login_sso_google',
+ 'login_sso_github',
+ 'login_email',
+ 'signup_sso_google',
+ 'signup_sso_github',
+ 'signup_email'
+]
+
+const DEFAULT_FORM_ACTION = 'login_email'
+
+const getFormAction = (signupTypeFlags) =>
+ FORM_ACTION_PRIORITY.find((action) => signupTypeFlags?.[action]) ?? DEFAULT_FORM_ACTION
+
export class SignInTracker {
/**
* Interface for TrackerAdapter.
@@ -14,23 +35,89 @@ export class SignInTracker {
}
/**
+ * @param {Object} payload
+ * @param {'google'|'azure'|'github'|'email'} payload.method
+ * @param {Object} [payload.signupTypeFlags] - Flags for signin type tracking
+ * @param {string} [payload.email] - User email for HubSpot
+ * @param {string} [payload.userId] - Console user ID for HubSpot
+ * @param {string} [payload.firstname] - User first name for HubSpot
+ * @param {string} [payload.lastname] - User last name for HubSpot
+ * @param {string} [payload.company] - Company name for HubSpot
+ * @param {string} [payload.githubHandle] - GitHub handle for HubSpot
+ * @param {string} [payload.phone] - User phone for HubSpot
+ *
* @returns {AnalyticsTrackerAdapter}
*/
- userSignedIn() {
+ userSignedIn(payload) {
+ // Get form_action from flags for both Segment and HubSpot
+ const formAction = getFormAction(payload.signupTypeFlags)
+
this.#trackerAdapter.addEvent({
eventName: 'User Signed In',
- props: {}
+ props: {
+ method: payload.method,
+ login_sso_google: payload.signupTypeFlags?.login_sso_google ?? false,
+ login_sso_github: payload.signupTypeFlags?.login_sso_github ?? false,
+ login_email: payload.signupTypeFlags?.login_email ?? false,
+ signup_sso_google: payload.signupTypeFlags?.signup_sso_google ?? false,
+ signup_sso_github: payload.signupTypeFlags?.signup_sso_github ?? false,
+ signup_email: payload.signupTypeFlags?.signup_email ?? false
+ }
+ })
+
+ // Submit to HubSpot if email and userId are provided
+ if (payload.email && payload.userId) {
+ hubspotFormSubmitService({
+ email: payload.email,
+ form_action: formAction,
+ user_id__rtm_: payload.userId,
+ segment_userid: payload.userId,
+ firstname: payload.firstname,
+ lastname: payload.lastname,
+ mobilephone: payload.phone,
+ company: payload.company,
+ github_handle: payload.githubHandle
+ })
+ }
+
+ return this.#trackerAdapter
+ }
+
+ /**
+ * @param {Object} payload
+ * @param {'google'|'azure'|'github'|'email'} payload.method
+ *
+ * @returns {AnalyticsTrackerAdapter}
+ */
+ userClickedSignIn(payload) {
+ this.#trackerAdapter.addEvent({
+ eventName: 'User Clicked to Sign In',
+ props: {
+ method: payload.method
+ }
})
return this.#trackerAdapter
}
/**
+ * @param {Object} payload
+ * @param {'google'|'azure'|'github'|'email'} payload.method
+ * @param {Object} [payload.signupTypeFlags] - Flags for signup type tracking
+ *
* @returns {AnalyticsTrackerAdapter}
*/
- userFailedSignIn() {
+ userFailedSignIn(payload) {
this.#trackerAdapter.addEvent({
eventName: 'User Failed to Sign In',
- props: {}
+ props: {
+ method: payload.method,
+ login_sso_google: payload.signupTypeFlags?.login_sso_google ?? false,
+ login_sso_github: payload.signupTypeFlags?.login_sso_github ?? false,
+ login_email: payload.signupTypeFlags?.login_email ?? false,
+ signup_sso_google: payload.signupTypeFlags?.signup_sso_google ?? false,
+ signup_sso_github: payload.signupTypeFlags?.signup_sso_github ?? false,
+ signup_email: payload.signupTypeFlags?.signup_email ?? false
+ }
})
return this.#trackerAdapter
}
diff --git a/src/plugins/analytics/trackers/SignUpTracker.js b/src/plugins/analytics/trackers/SignUpTracker.js
index 3582e53f59..4f8504827d 100644
--- a/src/plugins/analytics/trackers/SignUpTracker.js
+++ b/src/plugins/analytics/trackers/SignUpTracker.js
@@ -1,8 +1,25 @@
+import { hubspotFormSubmitService } from '@/services/hubspot-services'
+
+const FORM_ACTION_PRIORITY = [
+ 'signup_sso_google',
+ 'signup_sso_github',
+ 'signup_email',
+ 'login_sso_google',
+ 'login_sso_github',
+ 'login_email'
+]
+
+const DEFAULT_FORM_ACTION = 'signup_email'
+
+const getFormAction = (signupTypeFlags) =>
+ FORM_ACTION_PRIORITY.find((action) => signupTypeFlags?.[action]) ?? DEFAULT_FORM_ACTION
+
export class SignUpTracker {
/**
* Interface for TrackerAdapter.
* @typedef {Object} trackerAdapter
* @property {function({eventName: string, props: Object}): void} addEvent - Method to add an event.
+ * @property {function(): Object} getUserContext - Method to get user context for HubSpot.
*/
#trackerAdapter
@@ -16,16 +33,53 @@ export class SignUpTracker {
/**
* @param {Object} payload
* @param {'google'|'azure'|'github'|'email'} payload.method
+ * @param {string} [payload.firstSessionUrl] - The first session URL
+ * @param {Object} [payload.signupTypeFlags] - Flags for signup type tracking
+ * @param {string} [payload.email] - User email for HubSpot
+ * @param {string} [payload.userId] - Console user ID for HubSpot
+ * @param {string} [payload.firstname] - User first name for HubSpot
+ * @param {string} [payload.lastname] - User last name for HubSpot
+ * @param {string} [payload.company] - Company name for HubSpot
+ * @param {string} [payload.githubHandle] - GitHub handle for HubSpot
+ * @param {string} [payload.phone] - User phone for HubSpot
*
* @returns {AnalyticsTrackerAdapter}
*/
userSignedUp(payload) {
+ // Get signup_type from flags for both Segment and HubSpot
+ const signupType = getFormAction(payload.signupTypeFlags)
+
+ // Add Segment event
this.#trackerAdapter.addEvent({
eventName: 'User Signed Up',
props: {
- method: payload.method
+ method: payload.method,
+ first_session_url: payload.firstSessionUrl,
+ signup_type: signupType,
+ login_sso_google: payload.signupTypeFlags?.login_sso_google ?? false,
+ login_sso_github: payload.signupTypeFlags?.login_sso_github ?? false,
+ login_email: payload.signupTypeFlags?.login_email ?? false,
+ signup_sso_google: payload.signupTypeFlags?.signup_sso_google ?? false,
+ signup_sso_github: payload.signupTypeFlags?.signup_sso_github ?? false,
+ signup_email: payload.signupTypeFlags?.signup_email ?? false
}
})
+
+ // Submit to HubSpot if email and userId are provided
+ if (payload.email && payload.userId) {
+ hubspotFormSubmitService({
+ email: payload.email,
+ form_action: signupType,
+ user_id__rtm_: payload.userId,
+ segment_userid: payload.userId,
+ firstname: payload.firstname,
+ lastname: payload.lastname,
+ mobilephone: payload.phone,
+ company: payload.company,
+ github_handle: payload.githubHandle
+ })
+ }
+
return this.#trackerAdapter
}
@@ -48,14 +102,27 @@ export class SignUpTracker {
/**
* @param {Object} payload
* @param {'google'|'azure'|'github'} payload.method
+ * @param {string} [payload.firstSessionUrl] - The first session URL
+ * @param {Object} [payload.signupTypeFlags] - Flags for signup type tracking
*
* @returns {AnalyticsTrackerAdapter}
*/
userAuthorizedSso(payload) {
+ // Get signup_type from flags for consistency
+ const signupType = getFormAction(payload.signupTypeFlags)
+
this.#trackerAdapter.addEvent({
eventName: 'User Authorized SSO',
props: {
- method: payload.method
+ method: payload.method,
+ first_session_url: payload.firstSessionUrl,
+ signup_type: signupType,
+ login_sso_google: payload.signupTypeFlags?.login_sso_google ?? false,
+ login_sso_github: payload.signupTypeFlags?.login_sso_github ?? false,
+ login_email: payload.signupTypeFlags?.login_email ?? false,
+ signup_sso_google: payload.signupTypeFlags?.signup_sso_google ?? false,
+ signup_sso_github: payload.signupTypeFlags?.signup_sso_github ?? false,
+ signup_email: payload.signupTypeFlags?.signup_email ?? false
}
})
return this.#trackerAdapter
diff --git a/src/router/routes/signup-routes/index.js b/src/router/routes/signup-routes/index.js
index f12ec5e771..392ca07f8b 100644
--- a/src/router/routes/signup-routes/index.js
+++ b/src/router/routes/signup-routes/index.js
@@ -2,6 +2,7 @@ import * as SignupService from '@/services/signup-services'
import { inject } from 'vue'
import { useAccountStore } from '@/stores/account'
import SignupView from '@/views/Signup/SignupView.vue'
+import { getFirstSessionUrl } from '@/helpers/first-session-url'
/** @type {import('vue-router').RouteRecordRaw} */
export const signupRoutes = {
@@ -38,13 +39,50 @@ export const signupRoutes = {
beforeEnter: (__, ___, next) => {
const accountStore = useAccountStore()
const isFirstLogin = accountStore.isFirstLogin
+ const signupTypeFlags = accountStore.getSignupTypeFlags()
- if (isFirstLogin && accountStore.ssoSignUpMethod) {
+ // Check if this is a first login with signup tracking needed
+ const isEmailSignup = signupTypeFlags.signup_email
+ const isSsoSignup = accountStore.ssoSignUpMethod
+
+ if (isFirstLogin && (isSsoSignup || isEmailSignup)) {
/** @type {import('@/plugins/adapters/AnalyticsTrackerAdapter').AnalyticsTrackerAdapter} */
const tracker = inject('tracker')
- const signUpMethod = { method: accountStore.ssoSignUpMethod }
- tracker.signUp.userSignedUp(signUpMethod).signUp.userAuthorizedSso(signUpMethod)
+ // Determine the method for tracking
+ const method = isSsoSignup || 'email'
+
+ // Get user data for HubSpot tracking
+ const { userId: consoleUserId, accountData } = accountStore
+ const userEmail = accountData?.email
+ const userName = accountData?.name || ''
+ const companyName = accountData?.company_name || ''
+
+ // Parse first and last name from full name
+ const nameParts = userName.split(' ')
+ const firstname = nameParts[0] || ''
+ const lastname = nameParts.slice(1).join(' ') || ''
+
+ const signUpPayload = {
+ method,
+ firstSessionUrl: getFirstSessionUrl(),
+ signupTypeFlags,
+ // HubSpot required fields
+ email: userEmail,
+ userId: consoleUserId,
+ firstname,
+ lastname,
+ company: companyName,
+ githubHandle: method === 'github' ? accountData?.github_handle : undefined
+ }
+
+ // Track user signup with Segment and HubSpot
+ tracker.signUp.userSignedUp(signUpPayload)
+
+ // For SSO, also track the SSO authorization event
+ if (isSsoSignup) {
+ tracker.signUp.userAuthorizedSso(signUpPayload)
+ }
}
if (isFirstLogin) {
diff --git a/src/router/routes/switch-account-routes/index.js b/src/router/routes/switch-account-routes/index.js
index 0526773da0..ab4042de75 100644
--- a/src/router/routes/switch-account-routes/index.js
+++ b/src/router/routes/switch-account-routes/index.js
@@ -1,6 +1,9 @@
import { AccountHandler } from '@/helpers/account-handler'
import * as AuthServices from '@/services/auth-services'
import { listTypeAccountService } from '@/services/switch-account-services/list-type-account-service'
+import { useAccountStore } from '@/stores/account'
+import { loadUserAndAccountInfo } from '@/helpers/account-data'
+import { inject } from 'vue'
/** @type {import('vue-router').RouteRecordRaw} */
export const switchAccountRoutes = {
@@ -16,6 +19,9 @@ export const switchAccountRoutes = {
)
const verify = AuthServices.verifyAuthenticationService
const refresh = AuthServices.refreshAuthenticationService
+ /** @type {import('@/plugins/analytics/AnalyticsTrackerAdapter').AnalyticsTrackerAdapter} */
+ const tracker = inject('tracker')
+ const accountStore = useAccountStore()
try {
const EnableSocialLogin = true
@@ -24,6 +30,35 @@ export const switchAccountRoutes = {
refresh,
EnableSocialLogin
)
+
+ // Track SSO sign-in for returning users (not first login)
+ const signupTypeFlags = accountStore.getSignupTypeFlags()
+ const isSsoLogin = signupTypeFlags.login_sso_google || signupTypeFlags.login_sso_github
+
+ if (isSsoLogin) {
+ // Load user data to check firstLogin status and for HubSpot tracking
+ await loadUserAndAccountInfo()
+ const { userId: consoleUserId, accountData, isFirstLogin } = accountStore
+
+ // Only track sign-in for returning users (first login is tracked in signup-routes)
+ if (!isFirstLogin) {
+ const ssoMethod = signupTypeFlags.login_sso_google ? 'google' : 'github'
+
+ tracker.signIn
+ .userSignedIn({
+ method: ssoMethod,
+ signupTypeFlags,
+ email: accountData?.email,
+ userId: consoleUserId,
+ firstname: accountData?.first_name,
+ lastname: accountData?.last_name,
+ company: accountData?.company_name,
+ githubHandle: ssoMethod === 'github' ? accountData?.github_handle : undefined
+ })
+ .track()
+ }
+ }
+
next(redirect)
} catch {
next({ name: 'login' })
diff --git a/src/services/hubspot-services/hubspot-form-submit-service.js b/src/services/hubspot-services/hubspot-form-submit-service.js
new file mode 100644
index 0000000000..0ddc7a6eae
--- /dev/null
+++ b/src/services/hubspot-services/hubspot-form-submit-service.js
@@ -0,0 +1,113 @@
+import { AxiosHttpClientAdapter } from '../axios/AxiosHttpClientAdapter'
+
+const HUBSPOT_FORM_ID = 'b37e4a6a-8808-4e53-862a-7f82728138b9'
+const HUBSPOT_PORTAL_ID = '145499104'
+
+const REQUIRED_FIELDS = ['email', 'form_action', 'user_id__rtm_', 'segment_userid']
+
+const OPTIONAL_FIELDS_MAP = {
+ firstname: 'firstname',
+ lastname: 'lastname',
+ mobilephone: 'mobilephone',
+ company: 'company',
+ github_handle: 'github_handle'
+}
+
+const VALID_FORM_ACTIONS = [
+ 'signup_email',
+ 'signup_sso_google',
+ 'signup_sso_github',
+ 'login_email',
+ 'login_sso_google',
+ 'login_sso_github'
+]
+
+/**
+ * Validates the payload for required fields and valid form_action.
+ * @param {Object} payload - The form payload
+ * @throws {Error} If validation fails
+ */
+function validatePayload(payload) {
+ const missingFields = REQUIRED_FIELDS.filter((field) => !payload[field])
+
+ if (missingFields.length > 0) {
+ throw new Error(`Missing required fields: ${missingFields.join(', ')}`)
+ }
+
+ if (!VALID_FORM_ACTIONS.includes(payload.form_action)) {
+ throw new Error(
+ `Invalid form_action: ${payload.form_action}. Valid values: ${VALID_FORM_ACTIONS.join(', ')}`
+ )
+ }
+
+ if (payload.user_id__rtm_ !== payload.segment_userid) {
+ throw new Error('user_id__rtm_ must equal segment_userid')
+ }
+}
+
+/**
+ * Builds the fields array for HubSpot API from payload.
+ * @param {Object} payload - The form payload
+ * @returns {Array} Array of field objects
+ */
+function buildFields(payload) {
+ const requiredFields = REQUIRED_FIELDS.map((field) => ({
+ name: field,
+ value: payload[field]
+ }))
+
+ const optionalFields = Object.entries(OPTIONAL_FIELDS_MAP)
+ .filter(([key]) => payload[key])
+ .map(([key, fieldName]) => ({
+ name: fieldName,
+ value: payload[key]
+ }))
+
+ return [...requiredFields, ...optionalFields]
+}
+
+/**
+ * Submits a form to HubSpot Forms API.
+ * @param {Object} payload - The form payload
+ * @param {string} payload.email - User email (mandatory)
+ * @param {string} payload.form_action - Form action type (mandatory): signup_email | signup_sso_google | signup_sso_github | login_email | login_sso_google | login_sso_github
+ * @param {string} payload.user_id__rtm_ - Console user ID (mandatory, must equal segment_userid)
+ * @param {string} payload.segment_userid - Console user ID (mandatory)
+ * @param {string} [payload.firstname] - User first name
+ * @param {string} [payload.lastname] - User last name
+ * @param {string} [payload.mobilephone] - User phone
+ * @param {string} [payload.company] - Company name
+ * @param {string} [payload.github_handle] - GitHub handle
+ * @returns {Promise<{success: boolean, error?: string}>}
+ */
+export const hubspotFormSubmitService = async (payload) => {
+ try {
+ validatePayload(payload)
+
+ const fields = buildFields(payload)
+ const url = `https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_ID}`
+
+ await AxiosHttpClientAdapter.request({
+ url,
+ method: 'POST',
+ body: { fields },
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ return { success: true }
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
+
+ // Log error but don't block the signup flow
+ // eslint-disable-next-line no-console
+ console.warn('HubSpot form submission failed:', {
+ error: errorMessage,
+ email: payload.email,
+ form_action: payload.form_action
+ })
+
+ return { success: false, error: errorMessage }
+ }
+}
diff --git a/src/services/hubspot-services/index.js b/src/services/hubspot-services/index.js
new file mode 100644
index 0000000000..0263276df5
--- /dev/null
+++ b/src/services/hubspot-services/index.js
@@ -0,0 +1,3 @@
+import { hubspotFormSubmitService } from './hubspot-form-submit-service'
+
+export { hubspotFormSubmitService }
diff --git a/src/stores/account.js b/src/stores/account.js
index 56ee7761c1..666584325f 100644
--- a/src/stores/account.js
+++ b/src/stores/account.js
@@ -3,12 +3,20 @@ import { defineStore } from 'pinia'
export const useAccountStore = defineStore({
id: 'account',
persist: {
- paths: ['identifySignUpProvider', 'hasSession']
+ paths: ['identifySignUpProvider', 'hasSession', 'signupTypeFlags']
},
state: () => ({
account: {},
hasSession: false,
identifySignUpProvider: '',
+ signupTypeFlags: {
+ login_sso_google: false,
+ login_sso_github: false,
+ login_email: false,
+ signup_sso_google: false,
+ signup_sso_github: false,
+ signup_email: false
+ },
accountStatuses: {
BLOCKED: 'BLOCKED',
DEFAULTING: 'DEFAULTING',
@@ -146,12 +154,36 @@ export const useAccountStore = defineStore({
this.account = {}
this.hasSession = false
this.identifySignUpProvider = ''
+ this.signupTypeFlags = {
+ login_sso_google: false,
+ login_sso_github: false,
+ login_email: false,
+ signup_sso_google: false,
+ signup_sso_github: false,
+ signup_email: false
+ }
},
setSsoSignUpMethod(method) {
this.identifySignUpProvider = method
},
resetSsoSignUpMethod() {
this.identifySignUpProvider = ''
+ },
+ /**
+ * Sets a signup type flag to true.
+ * @param {string} flag - The flag name (e.g., 'signup_sso_google', 'login_email')
+ */
+ setSignupTypeFlag(flag) {
+ if (flag in this.signupTypeFlags) {
+ this.signupTypeFlags[flag] = true
+ }
+ },
+ /**
+ * Gets all signup type flags.
+ * @returns {Object} The signup type flags object
+ */
+ getSignupTypeFlags() {
+ return { ...this.signupTypeFlags }
}
}
})
diff --git a/src/templates/sign-in-block/index.vue b/src/templates/sign-in-block/index.vue
index 176cf99b67..3ec38cec41 100644
--- a/src/templates/sign-in-block/index.vue
+++ b/src/templates/sign-in-block/index.vue
@@ -77,7 +77,10 @@
-
+
@@ -160,11 +163,15 @@
import { useField, useForm } from 'vee-validate'
import { ref, inject, onMounted, computed, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
+ import { useAccountStore } from '@/stores/account'
+ import { loadUserAndAccountInfo } from '@/helpers/account-data'
+
import Divider from '@aziontech/webkit/divider'
import * as yup from 'yup'
import { useToast } from '@aziontech/webkit/use-toast'
/**@type {import('@/plugins/analytics/AnalyticsTrackerAdapter').AnalyticsTrackerAdapter} */
const tracker = inject('tracker')
+ const accountStore = useAccountStore()
defineOptions({ name: 'signInBlock' })
@@ -265,7 +272,22 @@
await props.authenticationLoginService(loginData)
const { twoFactor, trustedDevice, user_tracking_info: userInfo } = await verify()
- tracker.signIn.userSignedIn()
+ const signupTypeFlags = accountStore.getSignupTypeFlags()
+
+ // Load user and account info to populate accountStore for HubSpot tracking
+ await loadUserAndAccountInfo()
+ const { userId: consoleUserId, accountData } = accountStore
+ tracker.signIn
+ .userSignedIn({
+ method: 'email',
+ signupTypeFlags,
+ email: accountData?.email || values.email,
+ userId: consoleUserId,
+ firstname: accountData?.first_name || accountData?.name?.split(' ')[0],
+ lastname: accountData?.last_name || accountData?.name?.split(' ').slice(1).join(' '),
+ company: accountData?.company_name
+ })
+ .track()
if (twoFactor) {
const mfaRoute = trustedDevice ? 'authentication' : 'setup'
router.push(`/mfa/${mfaRoute}`)
@@ -274,7 +296,8 @@
await switchClientAccount(userInfo.props)
} catch {
- tracker.signIn.userFailedSignIn().track()
+ const signupTypeFlags = accountStore.getSignupTypeFlags()
+ tracker.signIn.userFailedSignIn({ method: 'email', signupTypeFlags }).track()
hasRequestErrorMessage.value = new UserNotFoundError().message
isButtonLoading.value = false
}
diff --git a/src/templates/signup-block/login-with-email-block.vue b/src/templates/signup-block/login-with-email-block.vue
index 2a9f9e0d48..dbcc15d76b 100644
--- a/src/templates/signup-block/login-with-email-block.vue
+++ b/src/templates/signup-block/login-with-email-block.vue
@@ -183,6 +183,9 @@
const handleSignUpEmailClick = () => {
const accountStore = useAccountStore()
accountStore.resetSsoSignUpMethod()
+ // Set signup_email flag when user chooses email signup
+ accountStore.setSignupTypeFlag('login_email')
+ accountStore.setSignupTypeFlag('signup_email')
showForm.value = true
labelButton.value = 'Sign Up'
}
diff --git a/src/templates/social-idps-block/index.vue b/src/templates/social-idps-block/index.vue
index ddd188422c..0623c4cff6 100644
--- a/src/templates/social-idps-block/index.vue
+++ b/src/templates/social-idps-block/index.vue
@@ -44,6 +44,14 @@
const tracker = inject('tracker')
+ const props = defineProps({
+ context: {
+ type: String,
+ default: 'signup',
+ validator: (value) => ['signup', 'login'].includes(value)
+ }
+ })
+
const idps = ref([])
const submittedIdp = ref(null)
@@ -86,8 +94,17 @@
if (validateOAuthRedirect(idp.loginUrl)) {
accountStore.setSsoSignUpMethod(idp.slug)
+ // Set the login_sso flag based on provider
+ const loginFlag = `login_sso_${idp.slug}`
+ accountStore.setSignupTypeFlag(loginFlag)
window.location.assign(idp.loginUrl)
- tracker.signUp.userClickedSignedUp({ method: idp.slug }).track()
+
+ // Track based on context (signup or login)
+ if (props.context === 'login') {
+ tracker.signIn.userClickedSignIn({ method: idp.slug }).track()
+ } else {
+ tracker.signUp.userClickedSignedUp({ method: idp.slug }).track()
+ }
} else {
loadingStore.finishLoading()
submittedIdp.value = null
diff --git a/src/tests/helpers/first-session-url.test.js b/src/tests/helpers/first-session-url.test.js
new file mode 100644
index 0000000000..6e70ef5d76
--- /dev/null
+++ b/src/tests/helpers/first-session-url.test.js
@@ -0,0 +1,151 @@
+import { describe, expect, it, beforeEach, vi } from 'vitest'
+import {
+ captureFirstSessionUrl,
+ getFirstSessionUrl,
+ clearFirstSessionUrl
+} from '@/helpers/first-session-url'
+
+const FIRST_SESSION_URL_KEY = 'azion_first_session_url'
+
+describe('first-session-url', () => {
+ beforeEach(() => {
+ localStorage.clear()
+ })
+
+ describe('captureFirstSessionUrl', () => {
+ it('should capture and store the current URL on first call', () => {
+ const testUrl = 'https://console.azion.com/signup?ref=docs'
+ vi.stubGlobal('location', { href: testUrl })
+
+ const result = captureFirstSessionUrl()
+
+ expect(result).toBe(testUrl)
+ expect(localStorage.getItem(FIRST_SESSION_URL_KEY)).toBe(testUrl)
+ })
+
+ it('should return existing URL on subsequent calls without overwriting', () => {
+ const firstUrl = 'https://console.azion.com/first-visit'
+ const secondUrl = 'https://console.azion.com/different-page'
+
+ // First capture
+ vi.stubGlobal('location', { href: firstUrl })
+ captureFirstSessionUrl()
+
+ // Second capture attempt with different URL
+ vi.stubGlobal('location', { href: secondUrl })
+ const result = captureFirstSessionUrl()
+
+ // Should return the first URL, not overwrite
+ expect(result).toBe(firstUrl)
+ expect(localStorage.getItem(FIRST_SESSION_URL_KEY)).toBe(firstUrl)
+ })
+
+ it('should return existing URL when called multiple times', () => {
+ const testUrl = 'https://console.azion.com/signup?utm_source=google'
+ vi.stubGlobal('location', { href: testUrl })
+
+ // Call multiple times
+ const result1 = captureFirstSessionUrl()
+ const result2 = captureFirstSessionUrl()
+ const result3 = captureFirstSessionUrl()
+
+ expect(result1).toBe(testUrl)
+ expect(result2).toBe(testUrl)
+ expect(result3).toBe(testUrl)
+ expect(localStorage.getItem(FIRST_SESSION_URL_KEY)).toBe(testUrl)
+ })
+ })
+
+ describe('getFirstSessionUrl', () => {
+ it('should return the stored first session URL', () => {
+ const testUrl = 'https://console.azion.com/signup'
+ localStorage.setItem(FIRST_SESSION_URL_KEY, testUrl)
+
+ const result = getFirstSessionUrl()
+
+ expect(result).toBe(testUrl)
+ })
+
+ it('should return null when no URL has been stored', () => {
+ const result = getFirstSessionUrl()
+
+ expect(result).toBeNull()
+ })
+
+ it('should not modify localStorage when retrieving', () => {
+ const testUrl = 'https://console.azion.com/signup'
+ localStorage.setItem(FIRST_SESSION_URL_KEY, testUrl)
+
+ getFirstSessionUrl()
+ getFirstSessionUrl()
+ getFirstSessionUrl()
+
+ expect(localStorage.getItem(FIRST_SESSION_URL_KEY)).toBe(testUrl)
+ })
+ })
+
+ describe('clearFirstSessionUrl', () => {
+ it('should remove the stored first session URL', () => {
+ const testUrl = 'https://console.azion.com/signup'
+ localStorage.setItem(FIRST_SESSION_URL_KEY, testUrl)
+
+ clearFirstSessionUrl()
+
+ expect(localStorage.getItem(FIRST_SESSION_URL_KEY)).toBeNull()
+ })
+
+ it('should not throw when no URL is stored', () => {
+ expect(() => clearFirstSessionUrl()).not.toThrow()
+ })
+
+ it('should allow capturing new URL after clearing', () => {
+ const firstUrl = 'https://console.azion.com/first'
+ const secondUrl = 'https://console.azion.com/second'
+
+ // Capture first URL
+ vi.stubGlobal('location', { href: firstUrl })
+ captureFirstSessionUrl()
+ expect(getFirstSessionUrl()).toBe(firstUrl)
+
+ // Clear
+ clearFirstSessionUrl()
+ expect(getFirstSessionUrl()).toBeNull()
+
+ // Capture new URL
+ vi.stubGlobal('location', { href: secondUrl })
+ captureFirstSessionUrl()
+ expect(getFirstSessionUrl()).toBe(secondUrl)
+ })
+ })
+
+ describe('integration flow', () => {
+ it('should handle full capture -> retrieve -> clear flow', () => {
+ const testUrl = 'https://console.azion.com/signup?utm_source=docs'
+
+ // Capture
+ vi.stubGlobal('location', { href: testUrl })
+ captureFirstSessionUrl()
+
+ // Retrieve
+ expect(getFirstSessionUrl()).toBe(testUrl)
+
+ // Clear
+ clearFirstSessionUrl()
+
+ // Verify cleared
+ expect(getFirstSessionUrl()).toBeNull()
+ })
+
+ it('should persist URL across multiple getFirstSessionUrl calls', () => {
+ const testUrl = 'https://console.azion.com/signup?ref=email'
+
+ vi.stubGlobal('location', { href: testUrl })
+ captureFirstSessionUrl()
+
+ // Multiple retrievals should return same value
+ expect(getFirstSessionUrl()).toBe(testUrl)
+ expect(getFirstSessionUrl()).toBe(testUrl)
+ expect(getFirstSessionUrl()).toBe(testUrl)
+ })
+ })
+})
diff --git a/src/tests/plugins/analytics/AnalyticsTrackerAdapter.test.js b/src/tests/plugins/analytics/AnalyticsTrackerAdapter.test.js
index 7094a9a067..0352cab4d2 100644
--- a/src/tests/plugins/analytics/AnalyticsTrackerAdapter.test.js
+++ b/src/tests/plugins/analytics/AnalyticsTrackerAdapter.test.js
@@ -208,11 +208,18 @@ describe('AnalyticsTrackerAdapter', () => {
it('should call userSigned when valid identification is provided', () => {
const { sut, analyticsClientSpy } = makeSut()
- sut.signIn.userSignedIn()
+ sut.signIn.userSignedIn({ method: 'email' })
sut.track()
expect(analyticsClientSpy.track).toHaveBeenCalledWith('User Signed In', {
+ method: 'email',
+ login_sso_google: false,
+ login_sso_github: false,
+ login_email: false,
+ signup_sso_google: false,
+ signup_sso_github: false,
+ signup_email: false,
application: fixtures.application
})
})
@@ -220,11 +227,18 @@ describe('AnalyticsTrackerAdapter', () => {
it('should call userFailedSignIn when valid identification is provided', () => {
const { sut, analyticsClientSpy } = makeSut()
- sut.signIn.userFailedSignIn()
+ sut.signIn.userFailedSignIn({ method: 'email' })
sut.track()
expect(analyticsClientSpy.track).toHaveBeenCalledWith('User Failed to Sign In', {
+ method: 'email',
+ login_sso_google: false,
+ login_sso_github: false,
+ login_email: false,
+ signup_sso_google: false,
+ signup_sso_github: false,
+ signup_email: false,
application: fixtures.application
})
})
@@ -251,10 +265,34 @@ describe('AnalyticsTrackerAdapter', () => {
it('should track the user sign-up event with the correct parameters', () => {
const { sut, analyticsClientSpy } = makeSut()
- sut.signUp.userSignedUp({ method: 'email' }).track()
+ const signupTypeFlags = {
+ login_sso_google: false,
+ login_sso_github: false,
+ login_email: true,
+ signup_sso_google: false,
+ signup_sso_github: false,
+ signup_email: true
+ }
+
+ sut.signUp
+ .userSignedUp({
+ method: 'email',
+ firstSessionUrl: 'https://example.com/signup',
+ signupType: 'email',
+ signupTypeFlags
+ })
+ .track()
expect(analyticsClientSpy.track).toHaveBeenCalledWith('User Signed Up', {
method: 'email',
+ first_session_url: 'https://example.com/signup',
+ signup_type: 'signup_email',
+ login_sso_google: false,
+ login_sso_github: false,
+ login_email: true,
+ signup_sso_google: false,
+ signup_sso_github: false,
+ signup_email: true,
application: fixtures.application
})
})
@@ -262,10 +300,34 @@ describe('AnalyticsTrackerAdapter', () => {
it('should track the user authorized sso event with the correct parameters', () => {
const { sut, analyticsClientSpy } = makeSut()
- sut.signUp.userAuthorizedSso({ method: 'google' }).track()
+ const signupTypeFlags = {
+ login_sso_google: true,
+ login_sso_github: false,
+ login_email: false,
+ signup_sso_google: true,
+ signup_sso_github: false,
+ signup_email: false
+ }
+
+ sut.signUp
+ .userAuthorizedSso({
+ method: 'google',
+ firstSessionUrl: 'https://example.com/signup',
+ signupType: 'sso',
+ signupTypeFlags
+ })
+ .track()
expect(analyticsClientSpy.track).toHaveBeenCalledWith('User Authorized SSO', {
method: 'google',
+ first_session_url: 'https://example.com/signup',
+ signup_type: 'signup_sso_google',
+ login_sso_google: true,
+ login_sso_github: false,
+ login_email: false,
+ signup_sso_google: true,
+ signup_sso_github: false,
+ signup_email: false,
application: fixtures.application
})
})
diff --git a/src/tests/plugins/analytics/trackers/SignInTracker.test.js b/src/tests/plugins/analytics/trackers/SignInTracker.test.js
new file mode 100644
index 0000000000..57435eaa51
--- /dev/null
+++ b/src/tests/plugins/analytics/trackers/SignInTracker.test.js
@@ -0,0 +1,292 @@
+import { SignInTracker } from '@/plugins/analytics/trackers/SignInTracker'
+import { describe, expect, it, vi, beforeEach } from 'vitest'
+
+vi.mock('@/services/hubspot-services', () => ({
+ hubspotFormSubmitService: vi.fn()
+}))
+
+const makeSut = () => {
+ const trackerAdapterSpy = {
+ addEvent: vi.fn().mockReturnThis()
+ }
+ const sut = new SignInTracker(trackerAdapterSpy)
+
+ return {
+ sut,
+ trackerAdapterSpy
+ }
+}
+
+describe('SignInTracker', () => {
+ let hubspotFormSubmitService
+
+ beforeEach(async () => {
+ vi.clearAllMocks()
+ hubspotFormSubmitService = (await import('@/services/hubspot-services'))
+ .hubspotFormSubmitService
+ })
+
+ describe('userSignedIn', () => {
+ it('should add Segment event with correct event name and props', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ sut.userSignedIn({
+ method: 'email',
+ signupTypeFlags: { login_email: true }
+ })
+
+ expect(trackerAdapterSpy.addEvent).toHaveBeenCalledWith({
+ eventName: 'User Signed In',
+ props: {
+ method: 'email',
+ login_sso_google: false,
+ login_sso_github: false,
+ login_email: true,
+ signup_sso_google: false,
+ signup_sso_github: false,
+ signup_email: false
+ }
+ })
+ })
+
+ it('should call HubSpot form submission when email and userId are provided', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedIn({
+ method: 'email',
+ signupTypeFlags: { login_email: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith({
+ email: 'test@example.com',
+ form_action: 'login_email',
+ user_id__rtm_: 'user-123',
+ segment_userid: 'user-123',
+ firstname: undefined,
+ lastname: undefined,
+ mobilephone: undefined,
+ company: undefined,
+ github_handle: undefined
+ })
+ })
+
+ it('should not call HubSpot when email is missing', () => {
+ const { sut } = makeSut()
+
+ sut.userSignedIn({
+ method: 'email',
+ userId: 'user-123',
+ signupTypeFlags: { login_email: true }
+ })
+
+ expect(hubspotFormSubmitService).not.toHaveBeenCalled()
+ })
+
+ it('should not call HubSpot when userId is missing', () => {
+ const { sut } = makeSut()
+
+ sut.userSignedIn({
+ method: 'email',
+ email: 'test@example.com',
+ signupTypeFlags: { login_email: true }
+ })
+
+ expect(hubspotFormSubmitService).not.toHaveBeenCalled()
+ })
+
+ it('should ensure user_id__rtm_ equals segment_userid', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedIn({
+ method: 'google',
+ signupTypeFlags: { login_sso_google: true },
+ email: 'test@example.com',
+ userId: 'console-user-456'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ user_id__rtm_: 'console-user-456',
+ segment_userid: 'console-user-456'
+ })
+ )
+ })
+
+ it('should pass all optional fields to HubSpot when provided', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedIn({
+ method: 'github',
+ signupTypeFlags: { login_sso_github: true },
+ email: 'test@example.com',
+ userId: 'user-789',
+ firstname: 'John',
+ lastname: 'Doe',
+ phone: '+1234567890',
+ company: 'Acme Inc',
+ githubHandle: 'johndoe'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith({
+ email: 'test@example.com',
+ form_action: 'login_sso_github',
+ user_id__rtm_: 'user-789',
+ segment_userid: 'user-789',
+ firstname: 'John',
+ lastname: 'Doe',
+ mobilephone: '+1234567890',
+ company: 'Acme Inc',
+ github_handle: 'johndoe'
+ })
+ })
+
+ it('should use correct form_action for login_sso_google', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedIn({
+ method: 'google',
+ signupTypeFlags: { login_sso_google: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ form_action: 'login_sso_google'
+ })
+ )
+ })
+
+ it('should use correct form_action for login_sso_github', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedIn({
+ method: 'github',
+ signupTypeFlags: { login_sso_github: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ form_action: 'login_sso_github'
+ })
+ )
+ })
+
+ it('should use correct form_action for login_email', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedIn({
+ method: 'email',
+ signupTypeFlags: { login_email: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ form_action: 'login_email'
+ })
+ )
+ })
+
+ it('should default to login_email when no signupTypeFlags provided', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedIn({
+ method: 'email',
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ form_action: 'login_email'
+ })
+ )
+ })
+
+ it('should return trackerAdapter for chaining', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ const result = sut.userSignedIn({
+ method: 'email',
+ signupTypeFlags: { login_email: true }
+ })
+
+ expect(result).toBe(trackerAdapterSpy)
+ })
+ })
+
+ describe('userClickedSignIn', () => {
+ it('should add Segment event with correct event name', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ sut.userClickedSignIn({ method: 'email' })
+
+ expect(trackerAdapterSpy.addEvent).toHaveBeenCalledWith({
+ eventName: 'User Clicked to Sign In',
+ props: {
+ method: 'email'
+ }
+ })
+ })
+
+ it('should return trackerAdapter for chaining', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ const result = sut.userClickedSignIn({ method: 'google' })
+
+ expect(result).toBe(trackerAdapterSpy)
+ })
+ })
+
+ describe('userFailedSignIn', () => {
+ it('should add Segment event with error details', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ sut.userFailedSignIn({
+ method: 'email',
+ signupTypeFlags: { login_email: true }
+ })
+
+ expect(trackerAdapterSpy.addEvent).toHaveBeenCalledWith({
+ eventName: 'User Failed to Sign In',
+ props: {
+ method: 'email',
+ login_sso_google: false,
+ login_sso_github: false,
+ login_email: true,
+ signup_sso_google: false,
+ signup_sso_github: false,
+ signup_email: false
+ }
+ })
+ })
+
+ it('should not call HubSpot', () => {
+ const { sut } = makeSut()
+
+ sut.userFailedSignIn({
+ method: 'email',
+ signupTypeFlags: { login_email: true }
+ })
+
+ expect(hubspotFormSubmitService).not.toHaveBeenCalled()
+ })
+
+ it('should return trackerAdapter for chaining', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ const result = sut.userFailedSignIn({
+ method: 'email',
+ signupTypeFlags: { login_email: true }
+ })
+
+ expect(result).toBe(trackerAdapterSpy)
+ })
+ })
+})
diff --git a/src/tests/plugins/analytics/trackers/SignUpTracker.test.js b/src/tests/plugins/analytics/trackers/SignUpTracker.test.js
new file mode 100644
index 0000000000..62bc0607f2
--- /dev/null
+++ b/src/tests/plugins/analytics/trackers/SignUpTracker.test.js
@@ -0,0 +1,471 @@
+import { SignUpTracker } from '@/plugins/analytics/trackers/SignUpTracker'
+import { describe, expect, it, vi, beforeEach } from 'vitest'
+
+vi.mock('@/services/hubspot-services', () => ({
+ hubspotFormSubmitService: vi.fn()
+}))
+
+const makeSut = () => {
+ const trackerAdapterSpy = {
+ addEvent: vi.fn().mockReturnThis()
+ }
+ const sut = new SignUpTracker(trackerAdapterSpy)
+
+ return {
+ sut,
+ trackerAdapterSpy
+ }
+}
+
+describe('SignUpTracker', () => {
+ let hubspotFormSubmitService
+
+ beforeEach(async () => {
+ vi.clearAllMocks()
+ hubspotFormSubmitService = (await import('@/services/hubspot-services'))
+ .hubspotFormSubmitService
+ })
+
+ describe('userSignedUp', () => {
+ it('should add Segment event with correct event name and props', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ sut.userSignedUp({
+ method: 'email',
+ firstSessionUrl: 'https://console.azion.com/signup?ref=docs',
+ signupTypeFlags: { signup_email: true }
+ })
+
+ expect(trackerAdapterSpy.addEvent).toHaveBeenCalledWith({
+ eventName: 'User Signed Up',
+ props: {
+ method: 'email',
+ first_session_url: 'https://console.azion.com/signup?ref=docs',
+ signup_type: 'signup_email',
+ login_sso_google: false,
+ login_sso_github: false,
+ login_email: false,
+ signup_sso_google: false,
+ signup_sso_github: false,
+ signup_email: true
+ }
+ })
+ })
+
+ it('should call HubSpot form submission when email and userId are provided', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'email',
+ firstSessionUrl: 'https://console.azion.com/signup',
+ signupTypeFlags: { signup_email: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith({
+ email: 'test@example.com',
+ form_action: 'signup_email',
+ user_id__rtm_: 'user-123',
+ segment_userid: 'user-123',
+ firstname: undefined,
+ lastname: undefined,
+ mobilephone: undefined,
+ company: undefined,
+ github_handle: undefined
+ })
+ })
+
+ it('should not call HubSpot when email is missing', () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'email',
+ userId: 'user-123',
+ signupTypeFlags: { signup_email: true }
+ })
+
+ expect(hubspotFormSubmitService).not.toHaveBeenCalled()
+ })
+
+ it('should not call HubSpot when userId is missing', () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'email',
+ email: 'test@example.com',
+ signupTypeFlags: { signup_email: true }
+ })
+
+ expect(hubspotFormSubmitService).not.toHaveBeenCalled()
+ })
+
+ it('should ensure user_id__rtm_ equals segment_userid', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'google',
+ firstSessionUrl: 'https://console.azion.com/signup',
+ signupTypeFlags: { signup_sso_google: true },
+ email: 'test@example.com',
+ userId: 'console-user-456'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ user_id__rtm_: 'console-user-456',
+ segment_userid: 'console-user-456'
+ })
+ )
+ })
+
+ it('should pass all optional fields to HubSpot when provided', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'github',
+ firstSessionUrl: 'https://console.azion.com/signup',
+ signupTypeFlags: { signup_sso_github: true },
+ email: 'test@example.com',
+ userId: 'user-789',
+ firstname: 'John',
+ lastname: 'Doe',
+ phone: '+1234567890',
+ company: 'Acme Inc',
+ githubHandle: 'johndoe'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith({
+ email: 'test@example.com',
+ form_action: 'signup_sso_github',
+ user_id__rtm_: 'user-789',
+ segment_userid: 'user-789',
+ firstname: 'John',
+ lastname: 'Doe',
+ mobilephone: '+1234567890',
+ company: 'Acme Inc',
+ github_handle: 'johndoe'
+ })
+ })
+
+ it('should use correct form_action for signup_sso_google', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'google',
+ signupTypeFlags: { signup_sso_google: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ form_action: 'signup_sso_google'
+ })
+ )
+ })
+
+ it('should use correct form_action for signup_sso_github', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'github',
+ signupTypeFlags: { signup_sso_github: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ form_action: 'signup_sso_github'
+ })
+ )
+ })
+
+ it('should use correct form_action for login_email', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'email',
+ signupTypeFlags: { login_email: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ form_action: 'login_email'
+ })
+ )
+ })
+
+ it('should use correct form_action for login_sso_google', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'google',
+ signupTypeFlags: { login_sso_google: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ form_action: 'login_sso_google'
+ })
+ )
+ })
+
+ it('should use correct form_action for login_sso_github', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'github',
+ signupTypeFlags: { login_sso_github: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ form_action: 'login_sso_github'
+ })
+ )
+ })
+
+ it('should default to signup_email when no signupTypeFlags provided', async () => {
+ const { sut } = makeSut()
+
+ sut.userSignedUp({
+ method: 'email',
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ expect(hubspotFormSubmitService).toHaveBeenCalledWith(
+ expect.objectContaining({
+ form_action: 'signup_email'
+ })
+ )
+ })
+
+ it('should return trackerAdapter for chaining', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ const result = sut.userSignedUp({
+ method: 'email',
+ signupTypeFlags: { signup_email: true }
+ })
+
+ expect(result).toBe(trackerAdapterSpy)
+ })
+ })
+
+ describe('userClickedSignedUp', () => {
+ it('should add Segment event with correct event name', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ sut.userClickedSignedUp({ method: 'email' })
+
+ expect(trackerAdapterSpy.addEvent).toHaveBeenCalledWith({
+ eventName: 'User Clicked to Sign Up',
+ props: {
+ method: 'email'
+ }
+ })
+ })
+
+ it('should return trackerAdapter for chaining', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ const result = sut.userClickedSignedUp({ method: 'google' })
+
+ expect(result).toBe(trackerAdapterSpy)
+ })
+ })
+
+ describe('userAuthorizedSso', () => {
+ it('should add Segment event with correct event name and props', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ sut.userAuthorizedSso({
+ method: 'google',
+ firstSessionUrl: 'https://console.azion.com/signup',
+ signupTypeFlags: { signup_sso_google: true }
+ })
+
+ expect(trackerAdapterSpy.addEvent).toHaveBeenCalledWith({
+ eventName: 'User Authorized SSO',
+ props: {
+ method: 'google',
+ first_session_url: 'https://console.azion.com/signup',
+ signup_type: 'signup_sso_google',
+ login_sso_google: false,
+ login_sso_github: false,
+ login_email: false,
+ signup_sso_google: true,
+ signup_sso_github: false,
+ signup_email: false
+ }
+ })
+ })
+
+ it('should not call HubSpot (only Segment event)', () => {
+ const { sut } = makeSut()
+
+ sut.userAuthorizedSso({
+ method: 'github',
+ signupTypeFlags: { signup_sso_github: true },
+ email: 'test@example.com',
+ userId: 'user-123'
+ })
+
+ // HubSpot should only be called by userSignedUp, not userAuthorizedSso
+ expect(hubspotFormSubmitService).not.toHaveBeenCalled()
+ })
+
+ it('should return trackerAdapter for chaining', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ const result = sut.userAuthorizedSso({ method: 'github' })
+
+ expect(result).toBe(trackerAdapterSpy)
+ })
+ })
+
+ describe('userFailedSignUp', () => {
+ it('should add Segment event with error details', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ sut.userFailedSignUp({
+ errorType: 'api',
+ fieldName: 'email',
+ errorMessage: 'Invalid email format'
+ })
+
+ expect(trackerAdapterSpy.addEvent).toHaveBeenCalledWith({
+ eventName: 'User Failed to Sign Up',
+ props: {
+ errorType: 'api',
+ fieldName: 'email',
+ errorMessage: 'Invalid email format'
+ }
+ })
+ })
+
+ it('should not call HubSpot', () => {
+ const { sut } = makeSut()
+
+ sut.userFailedSignUp({
+ errorType: 'field',
+ fieldName: 'password',
+ errorMessage: 'Password too short'
+ })
+
+ expect(hubspotFormSubmitService).not.toHaveBeenCalled()
+ })
+
+ it('should return trackerAdapter for chaining', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ const result = sut.userFailedSignUp({
+ errorType: 'api',
+ fieldName: 'email',
+ errorMessage: 'Error'
+ })
+
+ expect(result).toBe(trackerAdapterSpy)
+ })
+ })
+
+ describe('submittedAdditionalData', () => {
+ it('should add Segment event with additional data', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ sut.submittedAdditionalData({
+ use: 'Personal projects',
+ role: 'Developer',
+ inputRole: 'Senior Dev',
+ companySize: '1-10',
+ onboardingSession: 'scheduled',
+ companyWebsite: 'https://example.com',
+ fullName: 'John Doe'
+ })
+
+ expect(trackerAdapterSpy.addEvent).toHaveBeenCalledWith({
+ eventName: 'Submitted Additional Data',
+ props: {
+ use: 'Personal projects',
+ role: 'Developer',
+ inputRole: 'Senior Dev',
+ companySize: '1-10',
+ onboardingSchedule: 'scheduled',
+ website: 'https://example.com',
+ name: 'John Doe'
+ }
+ })
+ })
+
+ it('should return trackerAdapter for chaining', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ const result = sut.submittedAdditionalData({})
+
+ expect(result).toBe(trackerAdapterSpy)
+ })
+ })
+
+ describe('failedSubmitAdditionalData', () => {
+ it('should add Segment event with error details', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ sut.failedSubmitAdditionalData({
+ errorType: 'validation',
+ errorMessage: 'Field required',
+ fieldName: 'company'
+ })
+
+ expect(trackerAdapterSpy.addEvent).toHaveBeenCalledWith({
+ eventName: 'Failed to Submit Additional Data',
+ props: {
+ errorType: 'validation',
+ errorMessage: 'Field required',
+ fieldName: 'company'
+ }
+ })
+ })
+
+ it('should return trackerAdapter for chaining', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ const result = sut.failedSubmitAdditionalData({
+ errorType: 'api',
+ errorMessage: 'Error',
+ fieldName: 'role'
+ })
+
+ expect(result).toBe(trackerAdapterSpy)
+ })
+ })
+
+ describe('userActivatedAccount', () => {
+ it('should add Segment event', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ sut.userActivatedAccount()
+
+ expect(trackerAdapterSpy.addEvent).toHaveBeenCalledWith({
+ eventName: 'User Activated Account',
+ props: {}
+ })
+ })
+
+ it('should return trackerAdapter for chaining', () => {
+ const { sut, trackerAdapterSpy } = makeSut()
+
+ const result = sut.userActivatedAccount()
+
+ expect(result).toBe(trackerAdapterSpy)
+ })
+ })
+})
diff --git a/src/tests/services/hubspot-services/hubspot-form-submit-service.test.js b/src/tests/services/hubspot-services/hubspot-form-submit-service.test.js
new file mode 100644
index 0000000000..d290bfa39c
--- /dev/null
+++ b/src/tests/services/hubspot-services/hubspot-form-submit-service.test.js
@@ -0,0 +1,124 @@
+import { AxiosHttpClientAdapter } from '@/services/axios/AxiosHttpClientAdapter'
+import { hubspotFormSubmitService } from '@/services/hubspot-services/hubspot-form-submit-service'
+import { describe, expect, it, vi } from 'vitest'
+
+vi.mock('@/services/axios/AxiosHttpClientAdapter')
+
+describe('hubspotFormSubmitService', () => {
+ it('should submit form with mandatory fields only', async () => {
+ const requestSpy = vi.spyOn(AxiosHttpClientAdapter, 'request').mockResolvedValueOnce({
+ statusCode: 200
+ })
+
+ await hubspotFormSubmitService({
+ email: 'test@example.com',
+ form_action: 'signup_email',
+ user_id__rtm_: 'user-123',
+ segment_userid: 'user-123'
+ })
+
+ expect(requestSpy).toHaveBeenCalledWith({
+ url: 'https://api.hsforms.com/submissions/v3/integration/submit/145499104/b37e4a6a-8808-4e53-862a-7f82728138b9',
+ method: 'POST',
+ body: {
+ fields: [
+ { name: 'email', value: 'test@example.com' },
+ { name: 'form_action', value: 'signup_email' },
+ { name: 'user_id__rtm_', value: 'user-123' },
+ { name: 'segment_userid', value: 'user-123' }
+ ]
+ },
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ })
+
+ it('should submit form with all optional fields', async () => {
+ const requestSpy = vi.spyOn(AxiosHttpClientAdapter, 'request').mockResolvedValueOnce({
+ statusCode: 200
+ })
+
+ await hubspotFormSubmitService({
+ email: 'test@example.com',
+ form_action: 'signup_sso_google',
+ user_id__rtm_: 'user-456',
+ segment_userid: 'user-456',
+ firstname: 'John',
+ lastname: 'Doe',
+ mobilephone: '+1234567890',
+ company: 'Acme Inc',
+ github_handle: 'johndoe'
+ })
+
+ expect(requestSpy).toHaveBeenCalledWith({
+ url: 'https://api.hsforms.com/submissions/v3/integration/submit/145499104/b37e4a6a-8808-4e53-862a-7f82728138b9',
+ method: 'POST',
+ body: {
+ fields: [
+ { name: 'email', value: 'test@example.com' },
+ { name: 'form_action', value: 'signup_sso_google' },
+ { name: 'user_id__rtm_', value: 'user-456' },
+ { name: 'segment_userid', value: 'user-456' },
+ { name: 'firstname', value: 'John' },
+ { name: 'lastname', value: 'Doe' },
+ { name: 'mobilephone', value: '+1234567890' },
+ { name: 'company', value: 'Acme Inc' },
+ { name: 'github_handle', value: 'johndoe' }
+ ]
+ },
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ })
+
+ it('should handle errors gracefully without throwing', async () => {
+ vi.spyOn(AxiosHttpClientAdapter, 'request').mockRejectedValueOnce(new Error('Network error'))
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
+
+ // Should not throw
+ const result = await hubspotFormSubmitService({
+ email: 'test@example.com',
+ form_action: 'signup_email',
+ user_id__rtm_: 'user-123',
+ segment_userid: 'user-123'
+ })
+
+ expect(result).toEqual({ success: false, error: 'Network error' })
+
+ expect(consoleWarnSpy).toHaveBeenCalledWith('HubSpot form submission failed:', {
+ error: 'Network error',
+ email: 'test@example.com',
+ form_action: 'signup_email'
+ })
+ })
+
+ it('should use correct form_action values for different signup types', async () => {
+ const requestSpy = vi.spyOn(AxiosHttpClientAdapter, 'request').mockResolvedValue({
+ statusCode: 200
+ })
+
+ const formActions = [
+ 'signup_email',
+ 'signup_sso_google',
+ 'signup_sso_github',
+ 'login_email',
+ 'login_sso_google',
+ 'login_sso_github'
+ ]
+
+ for (const formAction of formActions) {
+ await hubspotFormSubmitService({
+ email: 'test@example.com',
+ form_action: formAction,
+ user_id__rtm_: 'user-123',
+ segment_userid: 'user-123'
+ })
+
+ const lastCall = requestSpy.mock.calls[requestSpy.mock.calls.length - 1]
+ const formActionField = lastCall[0].body.fields.find((field) => field.name === 'form_action')
+ expect(formActionField.value).toBe(formAction)
+ }
+ })
+})
diff --git a/src/views/Login/LoginView.vue b/src/views/Login/LoginView.vue
index c268336a62..c8fac20712 100644
--- a/src/views/Login/LoginView.vue
+++ b/src/views/Login/LoginView.vue
@@ -67,8 +67,11 @@
const isActivatedEmail = !!email && !activated
if (isActivatedEmail) {
+ // Set signup_email flag for tracking when user completes email signup
+ const accountStore = useAccountStore()
+ accountStore.setSignupTypeFlag('signup_email')
+
tracker.signUp.userActivatedAccount().track()
- tracker.signUp.userSignedUp({ method: 'email' }).track()
const newQuery = { ...route.query, activated: 'true' }
router.replace({ query: newQuery })
diff --git a/yarn.lock b/yarn.lock
index 5166badad7..0609199d8a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3713,10 +3713,9 @@ autoprefixer@^10.4.14:
resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz"
integrity sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==
dependencies:
- browserslist "^4.27.0"
- caniuse-lite "^1.0.30001754"
+ browserslist "^4.28.1"
+ caniuse-lite "^1.0.30001774"
fraction.js "^5.3.4"
- normalize-range "^0.1.2"
picocolors "^1.1.1"
postcss-value-parser "^4.2.0"
@@ -4063,7 +4062,7 @@ browserify-zlib@^0.2.0:
dependencies:
pako "~1.0.5"
-browserslist@^4.24.0, browserslist@^4.27.0, browserslist@^4.28.0:
+browserslist@^4.24.0, browserslist@^4.28.0:
version "4.28.0"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz"
integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==
@@ -4218,6 +4217,11 @@ caniuse-lite@^1.0.30001759:
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz"
integrity sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==
+caniuse-lite@^1.0.30001774:
+ version "1.0.30001787"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz#fd25c5e42e2d35df5c75eddda00d15d9c0c68f81"
+ integrity sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==
+
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz"