From adf39107847baccbcf3e8eeef6fdf878fe2a1346 Mon Sep 17 00:00:00 2001 From: Agrim Jain Date: Mon, 14 Oct 2024 14:23:34 +0400 Subject: [PATCH 1/5] fix: script in deriv app --- .../account-signup-modal.jsx | 28 +-- .../password-selection-modal.jsx | 17 +- .../core/src/Utils/Analytics/analytics.ts | 206 ++++++++++++++---- 3 files changed, 191 insertions(+), 60 deletions(-) diff --git a/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx b/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx index 54071cbfc9dd..01d6686c63df 100644 --- a/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx +++ b/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx @@ -16,7 +16,7 @@ import QuestionnaireModal from '../QuestionnaireModal'; import ResidenceForm from '../SetResidenceModal/set-residence-form.jsx'; import validateSignupFields from './validate-signup-fields.jsx'; import 'Sass/app/modules/account-signup.scss'; -import { trackEventWithCache } from 'Utils/Analytics/analytics.ts'; +import cacheTrackEvents from 'Utils/Analytics/analytics.ts'; const AccountSignup = ({ enableApp, @@ -56,20 +56,20 @@ const AccountSignup = ({ // didMount lifecycle hook React.useEffect(() => { - trackEventWithCache({ - name: 'ce_virtual_signup_form', - properties: { - action: 'signup_confirmed', - form_name: is_mobile ? 'virtual_signup_web_mobile_default' : 'virtual_signup_web_desktop_default', - }, - }); - trackEventWithCache({ - name: 'ce_virtual_signup_form', - properties: { - action: 'country_selection_screen_opened', - form_name: is_mobile ? 'virtual_signup_web_mobile_default' : 'virtual_signup_web_desktop_default', + const pageLoadEvent = { + page: 'onboarding', + event: { + name: 'ce_open_form', + properties: { + action: 'country_screen', + form_source: window.location.hostname, + form_name: 'default_diel_deriv', + url: window.location.href, + }, }, - }); + }; + + cacheTrackEvents.pageLoadEvent(pageLoadEvent); WS.wait('website_status', 'residence_list').then(() => { if (clients_country && residence_list) { diff --git a/packages/core/src/App/Containers/PasswordSelectionModal/password-selection-modal.jsx b/packages/core/src/App/Containers/PasswordSelectionModal/password-selection-modal.jsx index b2e999715ec7..1906db980fe4 100644 --- a/packages/core/src/App/Containers/PasswordSelectionModal/password-selection-modal.jsx +++ b/packages/core/src/App/Containers/PasswordSelectionModal/password-selection-modal.jsx @@ -6,7 +6,7 @@ import { Button, PasswordInput, PasswordMeter, Text } from '@deriv/components'; import { getErrorMessages, redirectToSignUp } from '@deriv/shared'; import { observer, useStore } from '@deriv/stores'; import { Localize, localize } from '@deriv/translations'; -import { trackEventWithCache } from 'Utils/Analytics/analytics.ts'; +import cacheTrackEvents from 'Utils/Analytics/analytics.ts'; import SignupSeparatorContainer from '../AccountSignupModal/signup-separator-container.jsx'; import 'Sass/app/modules/account-signup.scss'; @@ -29,11 +29,16 @@ const PasswordSelectionModal = observer( const { is_mobile } = ui; React.useEffect(() => { - trackEventWithCache({ - name: 'ce_virtual_signup_form', - properties: { - action: 'password_screen_opened', - form_name: is_mobile ? 'virtual_signup_web_mobile_default' : 'virtual_signup_web_desktop_default', + cacheTrackEvents.pageLoadEvent({ + page: 'onboarding', + event: { + name: 'ce_open_form', + properties: { + action: 'password_screen', + form_source: window.location.hostname, + form_name: 'default_diel_deriv', + url: window.location.href, + }, }, }); diff --git a/packages/core/src/Utils/Analytics/analytics.ts b/packages/core/src/Utils/Analytics/analytics.ts index f9beaf82f8d7..8e2379e55cac 100644 --- a/packages/core/src/Utils/Analytics/analytics.ts +++ b/packages/core/src/Utils/Analytics/analytics.ts @@ -1,48 +1,174 @@ -interface AnalyticsEvent { +import { Analytics } from '@deriv-com/analytics'; + +type ResponseData = { + url: string; + method: string; + status: number; + headers: string; + data: string; + payload: any; +}; + +type Event = { name: string; - properties: { - [key: string]: string; - }; -} - -const handleCachedEvents = () => { - let eventQueue: AnalyticsEvent[] = []; - const storedEvents = localStorage.getItem('pending_events'); - try { - if (storedEvents) { - eventQueue = JSON.parse(storedEvents) as AnalyticsEvent[]; - if (eventQueue.length > 0) { - eventQueue.forEach(event => { - window.rudderanalytics.track(event.name, event.properties); - }); + properties: Record; + cache?: boolean; +}; + +type Item = { + element: Element | string; + event: Event; + cache?: boolean; + callback?: (e: Event) => Event; +}; + +const cacheTrackEvents = { + interval: null as NodeJS.Timeout | null, + responses: [] as ResponseData[], + isTrackingResponses: false, + getCookies: (name: string): any => { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) { + const part = parts.pop(); + const cookieValue = part ? decodeURIComponent(part.split(';').shift()!) : null; - eventQueue = []; - localStorage.removeItem('pending_events'); + try { + return cookieValue ? JSON.parse(cookieValue) : null; + } catch (e) { + return cookieValue; } } - } catch (error) { - // eslint-disable-next-line no-console - console.log(error); - } -}; + return null; + }, + trackPageUnload: () => { + window.addEventListener('beforeunload', event => { + if (!cacheTrackEvents.isPageViewSent()) { + cacheTrackEvents.push('cached_analytics_page_views', { + name: window.location.href, + properties: { + url: window.location.href, + }, + }); + } + }); + }, -const setEvent = (event: AnalyticsEvent): void => { - const storedEvents = localStorage.getItem('pending_events'); - let eventQueue: AnalyticsEvent[] = []; - if (storedEvents) { - eventQueue = JSON.parse(storedEvents) as AnalyticsEvent[]; - } - eventQueue.push(event); - localStorage.setItem('pending_events', JSON.stringify(eventQueue)); -}; + isReady: (): boolean => { + if (typeof Analytics === 'undefined' || Analytics === null) { + return false; + } + + const instances = (window as any).Analytics.Analytics.getInstances(); + return !!instances?.tracking; + }, + parseCookies: (cookieName: string): any => { + const cookies = document.cookie.split('; ').reduce((acc: Record, cookie) => { + const [key, value] = cookie.split('='); + acc[decodeURIComponent(key)] = decodeURIComponent(value); + return acc; + }, {}); + + return JSON.parse(cookies[cookieName] || 'null'); + }, + isPageViewSent: (): boolean => + !!cacheTrackEvents.responses.find(e => e.payload?.type === 'page' && e.payload?.anonymousId), + set: (event: Event) => { + cacheTrackEvents.push('cached_analytics_events', event); + }, + push: (cookieName: string, data: any) => { + let storedCookies: any[] = []; + const cacheCookie = cacheTrackEvents.parseCookies(cookieName); + if (cacheCookie) storedCookies = cacheCookie; + storedCookies.push(data); + + document.cookie = `${cookieName}=${JSON.stringify(storedCookies)}; path=/; Domain=.deriv.com`; + }, + processEvent: (event: Event): Event => { + const clientInfo = cacheTrackEvents.getCookies('client_information'); + + if (clientInfo) { + const { email = null } = clientInfo; + } + + return event; + }, + track: (originalEvent: Event, cache: boolean) => { + const event = cacheTrackEvents.processEvent(originalEvent); + + if (cacheTrackEvents.isReady() && !cache) { + (window as any).Analytics.Analytics.trackEvent(event.name, event.properties); + } else { + cacheTrackEvents.set(event); + } + }, + pageView: () => { + if (!cacheTrackEvents.isTrackingResponses) { + cacheTrackEvents.trackPageUnload(); + } + + let pageViewInterval: NodeJS.Timeout | null = null; + + pageViewInterval = setInterval(() => { + if ( + (window as any).Analytics !== 'undefined' && + (window as any).Analytics.Analytics?.pageView === 'function' && + cacheTrackEvents.isReady() + ) { + (window as any).Analytics.pageView(window.location.href); + } + + if (cacheTrackEvents.isPageViewSent()) { + clearInterval(pageViewInterval!); + } + }, 1000); + }, + loadEvent: (items: Item[]) => { + items.forEach(({ event }) => { + const { name, properties } = event; + cacheTrackEvents.track( + { + name, + properties, + }, + false + ); + }); + + return cacheTrackEvents; + }, + pageLoadEvent: ( + items: Array<{ pages?: string[]; excludedPages?: string[]; event: Event; callback?: () => Event }> + ) => { + const pathname = window.location.pathname.slice(1); + + items.forEach(({ pages = [], excludedPages = [], event, callback = null }) => { + let dispatch = false; + if (pages.length) { + if (pages.includes(pathname)) { + dispatch = true; + } + } else if (excludedPages.length) { + if (!excludedPages.includes(pathname)) { + dispatch = true; + } + } else { + dispatch = true; + } + + if (dispatch) { + const eventData = callback ? callback() : event; + cacheTrackEvents.loadEvent([ + { + event: eventData, + element: '', + }, + ]); + } + }); -const trackEventWithCache = (event: AnalyticsEvent): void => { - if (window.rudderanalytics) { - handleCachedEvents(); - window.rudderanalytics.track(event.name, event.properties); - } else { - setEvent(event); - } + return cacheTrackEvents; + }, }; -export { trackEventWithCache, handleCachedEvents }; +export default cacheTrackEvents; From 37c959ac6af20d073eb0347e470fc8dcc4464638 Mon Sep 17 00:00:00 2001 From: Agrim Jain Date: Mon, 14 Oct 2024 15:19:47 +0400 Subject: [PATCH 2/5] fix: updates --- .../account-signup-modal.jsx | 1 - .../core/src/Utils/Analytics/analytics.ts | 34 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx b/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx index 01d6686c63df..ccb8097429c9 100644 --- a/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx +++ b/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx @@ -68,7 +68,6 @@ const AccountSignup = ({ }, }, }; - cacheTrackEvents.pageLoadEvent(pageLoadEvent); WS.wait('website_status', 'residence_list').then(() => { diff --git a/packages/core/src/Utils/Analytics/analytics.ts b/packages/core/src/Utils/Analytics/analytics.ts index 8e2379e55cac..040168ab35be 100644 --- a/packages/core/src/Utils/Analytics/analytics.ts +++ b/packages/core/src/Utils/Analytics/analytics.ts @@ -26,6 +26,28 @@ const cacheTrackEvents = { interval: null as NodeJS.Timeout | null, responses: [] as ResponseData[], isTrackingResponses: false, + hash: (inputString: string, desiredLength = 32): string => { + const fnv1aHash = (string: string): number => { + let hash = 0x811c9dc5; + for (let i = 0; i < string.length; i++) { + // eslint-disable-next-line no-bitwise + hash ^= string.charCodeAt(i); + hash = Math.floor((hash * 0x01000193) / Math.pow(2, 32)); + } + return hash; + }; + + const base64Encode = (string: string): string => btoa(string); + + const hash = fnv1aHash(inputString).toString(16); + let combined = base64Encode(hash); + + while (combined.length < desiredLength) { + combined += base64Encode(fnv1aHash(combined).toString(16)); + } + + return combined.substring(0, desiredLength); + }, getCookies: (name: string): any => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); @@ -89,6 +111,16 @@ const cacheTrackEvents = { if (clientInfo) { const { email = null } = clientInfo; + + if (email) { + event.properties.email_hash = cacheTrackEvents.hash(email); + } + } + + if (event?.properties?.email) { + const email = event.properties.email; + delete event.properties.email; + event.properties.email_hash = cacheTrackEvents.hash(email); } return event; @@ -161,7 +193,7 @@ const cacheTrackEvents = { cacheTrackEvents.loadEvent([ { event: eventData, - element: '', + element: '', // Assuming an empty string since it's not used }, ]); } From 57b856e0d4ea6cb0846fb4f59210b3fb4d4e196e Mon Sep 17 00:00:00 2001 From: Agrim Jain Date: Mon, 14 Oct 2024 15:38:06 +0400 Subject: [PATCH 3/5] fix: update --- .../Containers/AccountSignupModal/account-signup-modal.jsx | 5 +++++ packages/core/src/Utils/Analytics/analytics.ts | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx b/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx index ccb8097429c9..ce292ec6a68b 100644 --- a/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx +++ b/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx @@ -56,6 +56,8 @@ const AccountSignup = ({ // didMount lifecycle hook React.useEffect(() => { + // eslint-disable-next-line no-console + console.log('outside page load event'); const pageLoadEvent = { page: 'onboarding', event: { @@ -68,6 +70,9 @@ const AccountSignup = ({ }, }, }; + + // eslint-disable-next-line no-console + console.log('Tracking page load event:', pageLoadEvent); // Log the page load event cacheTrackEvents.pageLoadEvent(pageLoadEvent); WS.wait('website_status', 'residence_list').then(() => { diff --git a/packages/core/src/Utils/Analytics/analytics.ts b/packages/core/src/Utils/Analytics/analytics.ts index 040168ab35be..c5c280dff35b 100644 --- a/packages/core/src/Utils/Analytics/analytics.ts +++ b/packages/core/src/Utils/Analytics/analytics.ts @@ -32,7 +32,8 @@ const cacheTrackEvents = { for (let i = 0; i < string.length; i++) { // eslint-disable-next-line no-bitwise hash ^= string.charCodeAt(i); - hash = Math.floor((hash * 0x01000193) / Math.pow(2, 32)); + // eslint-disable-next-line no-bitwise + hash = (hash * 0x01000193) >>> 0; } return hash; }; @@ -174,6 +175,10 @@ const cacheTrackEvents = { ) => { const pathname = window.location.pathname.slice(1); + if (!Array.isArray(items)) { + return cacheTrackEvents; + } + items.forEach(({ pages = [], excludedPages = [], event, callback = null }) => { let dispatch = false; if (pages.length) { From 2bb9cb5c82f5b1e2c244f699126c5b8ccaac21c4 Mon Sep 17 00:00:00 2001 From: Agrim Jain Date: Thu, 17 Oct 2024 11:25:28 +0400 Subject: [PATCH 4/5] fix: types and tracking --- .../account-signup-modal.jsx | 40 +++++++----- .../password-selection-modal.jsx | 22 ++++--- .../core/src/Utils/Analytics/analytics.ts | 61 ++++++------------- 3 files changed, 55 insertions(+), 68 deletions(-) diff --git a/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx b/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx index ce292ec6a68b..64133616b3bf 100644 --- a/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx +++ b/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx @@ -57,23 +57,35 @@ const AccountSignup = ({ // didMount lifecycle hook React.useEffect(() => { // eslint-disable-next-line no-console - console.log('outside page load event'); - const pageLoadEvent = { - page: 'onboarding', - event: { - name: 'ce_open_form', - properties: { - action: 'country_screen', - form_source: window.location.hostname, - form_name: 'default_diel_deriv', - url: window.location.href, + cacheTrackEvents.pageLoadEvent([ + { + page: 'onboarding', + event: { + name: 'ce_virtual_signup_form', + properties: { + action: 'signup_confirmed', + form_name: is_mobile + ? 'virtual_signup_web_mobile_default' + : 'virtual_signup_web_desktop_default', + }, }, }, - }; + ]); - // eslint-disable-next-line no-console - console.log('Tracking page load event:', pageLoadEvent); // Log the page load event - cacheTrackEvents.pageLoadEvent(pageLoadEvent); + cacheTrackEvents.pageLoadEvent([ + { + page: 'onboarding', + event: { + name: 'ce_virtual_signup_form', + properties: { + action: 'country_selection_screen_opened', + form_name: is_mobile + ? 'virtual_signup_web_mobile_default' + : 'virtual_signup_web_desktop_default', + }, + }, + }, + ]); WS.wait('website_status', 'residence_list').then(() => { if (clients_country && residence_list) { diff --git a/packages/core/src/App/Containers/PasswordSelectionModal/password-selection-modal.jsx b/packages/core/src/App/Containers/PasswordSelectionModal/password-selection-modal.jsx index 1906db980fe4..0db86a30e797 100644 --- a/packages/core/src/App/Containers/PasswordSelectionModal/password-selection-modal.jsx +++ b/packages/core/src/App/Containers/PasswordSelectionModal/password-selection-modal.jsx @@ -29,18 +29,20 @@ const PasswordSelectionModal = observer( const { is_mobile } = ui; React.useEffect(() => { - cacheTrackEvents.pageLoadEvent({ - page: 'onboarding', - event: { - name: 'ce_open_form', - properties: { - action: 'password_screen', - form_source: window.location.hostname, - form_name: 'default_diel_deriv', - url: window.location.href, + cacheTrackEvents.pageLoadEvent([ + { + page: 'onboarding', + event: { + name: 'ce_virtual_signup_form', + properties: { + action: 'password_screen_opened', + form_name: is_mobile + ? 'virtual_signup_web_mobile_default' + : 'virtual_signup_web_desktop_default', + }, }, }, - }); + ]); // Analytics.trackEvent('ce_virtual_signup_form', { // action: 'password_screen_opened', diff --git a/packages/core/src/Utils/Analytics/analytics.ts b/packages/core/src/Utils/Analytics/analytics.ts index c5c280dff35b..3e8610dfadf5 100644 --- a/packages/core/src/Utils/Analytics/analytics.ts +++ b/packages/core/src/Utils/Analytics/analytics.ts @@ -1,27 +1,28 @@ import { Analytics } from '@deriv-com/analytics'; +interface Payload { + type: string; + anonymousId: string; +} + type ResponseData = { url: string; method: string; status: number; headers: string; data: string; - payload: any; + payload: Payload; }; - type Event = { name: string; - properties: Record; + properties: Record; cache?: boolean; }; - type Item = { - element: Element | string; event: Event; cache?: boolean; callback?: (e: Event) => Event; }; - const cacheTrackEvents = { interval: null as NodeJS.Timeout | null, responses: [] as ResponseData[], @@ -37,25 +38,20 @@ const cacheTrackEvents = { } return hash; }; - const base64Encode = (string: string): string => btoa(string); - const hash = fnv1aHash(inputString).toString(16); let combined = base64Encode(hash); - while (combined.length < desiredLength) { combined += base64Encode(fnv1aHash(combined).toString(16)); } - return combined.substring(0, desiredLength); }, - getCookies: (name: string): any => { + getCookies: (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) { const part = parts.pop(); const cookieValue = part ? decodeURIComponent(part.split(';').shift()!) : null; - try { return cookieValue ? JSON.parse(cookieValue) : null; } catch (e) { @@ -76,22 +72,19 @@ const cacheTrackEvents = { } }); }, - isReady: (): boolean => { if (typeof Analytics === 'undefined' || Analytics === null) { return false; } - - const instances = (window as any).Analytics.Analytics.getInstances(); + const instances = Analytics?.getInstances(); return !!instances?.tracking; }, - parseCookies: (cookieName: string): any => { + parseCookies: (cookieName: string): Event[] | null => { const cookies = document.cookie.split('; ').reduce((acc: Record, cookie) => { const [key, value] = cookie.split('='); acc[decodeURIComponent(key)] = decodeURIComponent(value); return acc; }, {}); - return JSON.parse(cookies[cookieName] || 'null'); }, isPageViewSent: (): boolean => @@ -99,38 +92,32 @@ const cacheTrackEvents = { set: (event: Event) => { cacheTrackEvents.push('cached_analytics_events', event); }, - push: (cookieName: string, data: any) => { - let storedCookies: any[] = []; + push: (cookieName: string, data: Event) => { + let storedCookies: Event[] = []; const cacheCookie = cacheTrackEvents.parseCookies(cookieName); if (cacheCookie) storedCookies = cacheCookie; storedCookies.push(data); - document.cookie = `${cookieName}=${JSON.stringify(storedCookies)}; path=/; Domain=.deriv.com`; }, processEvent: (event: Event): Event => { const clientInfo = cacheTrackEvents.getCookies('client_information'); - if (clientInfo) { - const { email = null } = clientInfo; - + const { email = null } = JSON.parse(clientInfo); if (email) { event.properties.email_hash = cacheTrackEvents.hash(email); } } - if (event?.properties?.email) { const email = event.properties.email; delete event.properties.email; event.properties.email_hash = cacheTrackEvents.hash(email); } - return event; }, track: (originalEvent: Event, cache: boolean) => { - const event = cacheTrackEvents.processEvent(originalEvent); - + const event: any = cacheTrackEvents.processEvent(originalEvent); if (cacheTrackEvents.isReady() && !cache) { - (window as any).Analytics.Analytics.trackEvent(event.name, event.properties); + Analytics?.trackEvent(event.name, event.properties); } else { cacheTrackEvents.set(event); } @@ -139,18 +126,11 @@ const cacheTrackEvents = { if (!cacheTrackEvents.isTrackingResponses) { cacheTrackEvents.trackPageUnload(); } - let pageViewInterval: NodeJS.Timeout | null = null; - pageViewInterval = setInterval(() => { - if ( - (window as any).Analytics !== 'undefined' && - (window as any).Analytics.Analytics?.pageView === 'function' && - cacheTrackEvents.isReady() - ) { - (window as any).Analytics.pageView(window.location.href); + if (Analytics !== undefined && typeof Analytics?.pageView === 'function' && cacheTrackEvents.isReady()) { + Analytics?.pageView(window.location.href); } - if (cacheTrackEvents.isPageViewSent()) { clearInterval(pageViewInterval!); } @@ -167,18 +147,15 @@ const cacheTrackEvents = { false ); }); - return cacheTrackEvents; }, pageLoadEvent: ( items: Array<{ pages?: string[]; excludedPages?: string[]; event: Event; callback?: () => Event }> ) => { const pathname = window.location.pathname.slice(1); - if (!Array.isArray(items)) { return cacheTrackEvents; } - items.forEach(({ pages = [], excludedPages = [], event, callback = null }) => { let dispatch = false; if (pages.length) { @@ -192,20 +169,16 @@ const cacheTrackEvents = { } else { dispatch = true; } - if (dispatch) { const eventData = callback ? callback() : event; cacheTrackEvents.loadEvent([ { event: eventData, - element: '', // Assuming an empty string since it's not used }, ]); } }); - return cacheTrackEvents; }, }; - export default cacheTrackEvents; From 96c9d0b6003636eb1fc24392ce6feaa71c166b49 Mon Sep 17 00:00:00 2001 From: Agrim Jain Date: Thu, 17 Oct 2024 11:29:39 +0400 Subject: [PATCH 5/5] fix: empty commit