diff --git a/src/hooks/useHubspot.ts b/src/hooks/useHubspot.ts index 992914d14..8c9825e4b 100644 --- a/src/hooks/useHubspot.ts +++ b/src/hooks/useHubspot.ts @@ -1,45 +1,160 @@ // inspired by: https://github.com/kelvinmaues/react-hubspot-tracking-code-hook +import * as Sentry from "@sentry/react"; + import { PropsUseSetTrackEvent, UseTrackingCode } from "@src/types/hooks"; export const useHubspot = (): UseTrackingCode => { const _hsq = typeof window !== "undefined" && window._hsq ? window._hsq : []; const setContentType = (contentType: string): void => { - _hsq.push(["setContentType", contentType]); + try { + if (!contentType || contentType.trim() === "") { + Sentry.captureMessage("HubSpot setContentType failed: empty contentType", { + level: "warning", + tags: { component: "hubspot-tracking" }, + extra: { contentType, hasHsq: !!_hsq }, + }); + return; + } + _hsq.push(["setContentType", contentType]); + } catch (error) { + Sentry.captureException(error, { + tags: { component: "hubspot-tracking" }, + extra: { contentType, hasHsq: !!_hsq }, + level: "error", + }); + } }; const setTrackPageView = () => { - _hsq.push(["trackPageView"]); + try { + _hsq.push(["trackPageView"]); + } catch (error) { + Sentry.captureException(error, { + tags: { component: "hubspot-tracking" }, + extra: { hasHsq: !!_hsq }, + level: "error", + }); + } }; const setPathPageView = (path: string): void => { - _hsq.push(["setPath", path]); - setTrackPageView(); + try { + if (!path || path.trim() === "") { + Sentry.captureMessage("HubSpot setPathPageView failed: empty path", { + level: "warning", + tags: { component: "hubspot-tracking" }, + extra: { path, hasHsq: !!_hsq }, + }); + return; + } + _hsq.push(["setPath", path]); + setTrackPageView(); + } catch (error) { + Sentry.captureException(error, { + tags: { component: "hubspot-tracking" }, + extra: { path, hasHsq: !!_hsq }, + level: "error", + }); + } }; const setIdentity = (email: string, customPropertities?: object) => { - _hsq.push([ - "identify", - { - email, - ...customPropertities, - }, - ]); + try { + if (!email || email.trim() === "") { + Sentry.captureMessage("HubSpot setIdentity failed: empty email", { + level: "warning", + tags: { component: "hubspot-tracking" }, + extra: { + email, + hasCustomProperties: !!customPropertities, + hasHsq: !!_hsq, + }, + }); + return; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + Sentry.captureMessage("HubSpot setIdentity failed: invalid email format", { + level: "warning", + tags: { component: "hubspot-tracking" }, + extra: { + email, + hasCustomProperties: !!customPropertities, + hasHsq: !!_hsq, + }, + }); + return; + } + + _hsq.push([ + "identify", + { + email, + ...customPropertities, + }, + ]); + } catch (error) { + Sentry.captureException(error, { + tags: { component: "hubspot-tracking" }, + extra: { + email, + hasCustomProperties: !!customPropertities, + customPropertities, + hasHsq: !!_hsq, + }, + level: "error", + }); + } }; const setTrackEvent = ({ eventId, value }: PropsUseSetTrackEvent) => { - _hsq.push([ - "trackEvent", - { - id: eventId, - value, - }, - ]); + try { + if (!eventId || eventId.trim() === "") { + Sentry.captureMessage("HubSpot setTrackEvent failed: empty eventId", { + level: "warning", + tags: { component: "hubspot-tracking" }, + extra: { + eventId, + value, + hasHsq: !!_hsq, + }, + }); + return; + } + + _hsq.push([ + "trackEvent", + { + id: eventId, + value, + }, + ]); + } catch (error) { + Sentry.captureException(error, { + tags: { component: "hubspot-tracking" }, + extra: { + eventId, + value, + hasHsq: !!_hsq, + }, + level: "error", + }); + } }; const revokeCookieConsent = () => { - _hsq.push(["revokeCookieConsent"]); + try { + _hsq.push(["revokeCookieConsent"]); + } catch (error) { + Sentry.captureException(error, { + tags: { component: "hubspot-tracking" }, + extra: { hasHsq: !!_hsq }, + level: "error", + }); + } }; return { diff --git a/src/hooks/useHubspotSubmission.ts b/src/hooks/useHubspotSubmission.ts index c8e82c0d7..14207a8c1 100644 --- a/src/hooks/useHubspotSubmission.ts +++ b/src/hooks/useHubspotSubmission.ts @@ -1,3 +1,4 @@ +import * as Sentry from "@sentry/react"; import Cookies from "js-cookie"; import { isProduction, hubSpotFormId, hubSpotPortalId, namespaces } from "@constants"; @@ -6,31 +7,188 @@ import { LoggerService } from "@services/logger.service"; export function useHubspotSubmission({ t }: HubspotSubmissionArgs) { return async (user: { email?: string; name?: string }) => { - if (!isProduction || !hubSpotPortalId || !hubSpotFormId) return; + if (!isProduction) return; + if (!hubSpotPortalId || !hubSpotFormId) { + const message = "HubSpot submission skipped: missing formId or portalId"; + LoggerService.error(namespaces.ui.loginPage, message, true); + Sentry.captureMessage(message, { + level: "error", + tags: { component: "hubspot-submission-on-login" }, + extra: { + isProduction, + hasHubSpotPortalId: !!hubSpotPortalId, + hasHubSpotFormId: !!hubSpotFormId, + }, + }); + + return; + } + if (!user?.email) { + const message = "HubSpot submission skipped: missing user.email"; + LoggerService.error(namespaces.ui.loginPage, message, true); + Sentry.captureMessage(message, { + level: "error", + tags: { component: "hubspot-submission-on-login" }, + extra: { + hasUser: !!user, + hasUserName: !!user?.name, + }, + }); + return; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(user.email)) { + const errorMessage = "HubSpot submission skipped: invalid email format"; + LoggerService.error(namespaces.ui.loginPage, errorMessage, true); + Sentry.captureMessage(errorMessage, { + level: "error", + tags: { component: "hubspot-submission-on-login" }, + extra: { userEmail: user.email }, + }); + return; + } const hsUrl = `https://api.hsforms.com/submissions/v3/integration/submit/${hubSpotPortalId}/${hubSpotFormId}`; + + const hubspotUtk = Cookies.get("hubspotutk"); + const pageUri = window.location.href; + const pageName = document.title; + + if (!hubspotUtk || hubspotUtk.trim() === "" || !pageUri || !pageName) { + const missingValues = []; + if (!hubspotUtk || hubspotUtk.trim() === "") missingValues.push("hubspotutk"); + if (!pageUri) missingValues.push("pageUri"); + if (!pageName) missingValues.push("pageName"); + + const message = `HubSpot submission skipped: missing required values: ${missingValues.join(", ")}`; + LoggerService.error(namespaces.ui.loginPage, message, true); + Sentry.captureMessage(message, { + level: "warning", + tags: { component: "hubspot-submission-on-login" }, + extra: { + missingValues, + hasHubspotUtk: !!hubspotUtk, + hasPageUri: !!pageUri, + hasPageName: !!pageName, + userEmail: user.email, + userName: user.name, + }, + }); + return; + } + + if (!user.name || user.name.trim() === "") { + const message = "HubSpot submission: user name is empty"; + LoggerService.error(namespaces.ui.loginPage, message, true); + Sentry.captureMessage(message, { + level: "warning", + tags: { component: "hubspot-submission-on-login" }, + extra: { + userEmail: user.email, + userName: user.name, + }, + }); + } + const hsContext = { - hutk: Cookies.get("hubspotutk"), - pageUri: window.location.href, - pageName: document.title, + hutk: hubspotUtk, + pageUri: pageUri, + pageName: pageName, }; const hsData = [ - { objectTypeId: "0-1", name: "email", value: user?.email }, - { objectTypeId: "0-1", name: "firstname", value: user?.name }, + { name: "email", value: user.email }, + { name: "firstname", value: user.name }, ]; const submissionData = { submittedAt: Date.now(), fields: hsData, context: hsContext, }; + try { - await fetch(hsUrl, { + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => { + controller.abort(); + }, 8000); + + const res = await fetch(hsUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(submissionData), + signal: controller.signal, + }); + + window.clearTimeout(timeoutId); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + const errorMessage = `HubSpot submission failed: ${res.status} ${res.statusText}${text ? " - " + text : ""}`; + + LoggerService.error(namespaces.ui.loginPage, errorMessage, true); + + Sentry.captureException(new Error(errorMessage), { + tags: { + component: "hubspot-submission-on-login", + http_status: res.status, + http_status_text: res.statusText, + }, + extra: { + userEmail: user.email, + userName: user.name, + hubSpotUrl: hsUrl, + responseText: text, + submissionData: submissionData, + }, + level: "error", + }); + return; + } + + const successMessage = "HubSpot submission succeeded"; + LoggerService.debug(namespaces.ui.loginPage, successMessage); + Sentry.captureMessage(successMessage, { + level: "info", + tags: { component: "hubspot-submission-on-login" }, + extra: { + userEmail: user.email, + userName: user.name, + responseStatus: res.status, + }, + }); + } catch (error: any) { + let errorMessage: string; + let errorType = "UnknownError"; + + if (error.name === "AbortError") { + errorMessage = "HubSpot submission timed out after 8 seconds"; + errorType = "TimeoutError"; + } else if (error.name === "TypeError" && error.message.includes("fetch")) { + errorMessage = "HubSpot submission failed: network error"; + errorType = "NetworkError"; + } else { + errorMessage = t("errors.loginFailedExtended", { error: String(error?.message ?? error) }); + errorType = error.name || "UnknownError"; + } + + LoggerService.error(namespaces.ui.loginPage, errorMessage, true); + + Sentry.captureException(error, { + tags: { + component: "hubspot-submission-on-login", + error_type: errorType, + }, + extra: { + userEmail: user.email, + userName: user.name, + hubSpotUrl: hsUrl, + submissionData: submissionData, + errorMessage: error.message, + errorStack: error.stack, + timeout: errorType === "TimeoutError", + }, + level: "error", }); - } catch (error) { - LoggerService.error(namespaces.ui.loginPage, t("errors.loginFailedExtended", { error }), true); } }; }