Skip to content

Commit

Permalink
Adrienne / Rudderstack integration (binary-com#9013)
Browse files Browse the repository at this point in the history
* feat: integrated rudderstack hook

* feat: added test cases for use-rudderstack hook

* refactor: moved rudderstack out of store and into utils

* chore: updated conditions

* chore: removed rudderstack script html

* chore: removed test functions

* chore: removed test functions

* chore: removed old changes

* chore: refactored code based on reviews

* chore: test commit

* chore: removed package-lock.json

* chore: added optional for tab_name

* feat: moved analytics into its own package

* chore: incorporated code review changes

* chore: removed changed files from merge conflict

* chore: removed changed files from merge conflict

* chore: removed prettier changes

* chore: reverted changes from reviews

* chore: incorporated code review changes

* fix: separated env variables for ruddetstack

* refactor: updated conditions

---------

Co-authored-by: Ali(Ako) Hosseini <ali.hosseini@deriv.com>
Co-authored-by: Jim Daniels Wasswa <104334373+jim-deriv@users.noreply.github.com>
  • Loading branch information
3 people committed Jul 5, 2023
1 parent 83022b9 commit 37b843d
Show file tree
Hide file tree
Showing 16 changed files with 282 additions and 142 deletions.
7 changes: 7 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ commands:
- "packages/trader/node_modules"
- "packages/translations/node_modules"
- "packages/utils/node_modules"
- "packages/analytics/node_modules"
# VERIFY_CACHE_FOLDERS_END (DO NOT REMOVE)

build:
Expand Down Expand Up @@ -264,6 +265,9 @@ jobs:
- run:
name: "Check TypeScript for @deriv/utils"
command: npx tsc --project packages/utils/tsconfig.json -noEmit
- run:
name: "Check TypeScript for @deriv/analytics"
command: npx tsc --project packages/analytics/tsconfig.json -noEmit
- run:
name: "Check TypeScript for @deriv/stores"
command: npx tsc --project packages/stores/tsconfig.json -noEmit
Expand All @@ -276,6 +280,9 @@ jobs:
- run:
name: "Check tests for @deriv/utils"
command: bash ./scripts/check-tests.sh packages/utils/src
- run:
name: "Check tests for @deriv/analytics"
command: bash ./scripts/check-tests.sh packages/analytics/src
- build
- run:
name: "Run tests"
Expand Down
5 changes: 5 additions & 0 deletions packages/analytics/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const baseConfigForPackages = require('../../jest.config.base');

module.exports = {
...baseConfigForPackages,
};
12 changes: 12 additions & 0 deletions packages/analytics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@deriv/analytics",
"private": true,
"version": "1.0.0",
"main": "src/index.ts",
"devDependencies": {
"typescript": "^4.6.3"
},
"dependencies": {
"rudder-sdk-js": "^2.35.0"
}
}
65 changes: 65 additions & 0 deletions packages/analytics/src/__tests__/rudderstack.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { RudderStack } from '../rudderstack';

jest.mock('rudder-sdk-js', () => {
const original_module = jest.requireActual('rudder-sdk-js');
return {
...original_module,
load: jest.fn(),
ready: (callback: () => any) => callback(),
track: jest.fn(),
};
});

describe('rudderstack', () => {
let rudderstack: RudderStack;
const originalEnv = process.env;

beforeAll(() => {
process.env = {
...originalEnv,
CIRCLE_JOB: 'release_staging',
RUDDERSTACK_PRODUCTION_KEY: '123456789',
RUDDERSTACK_STAGING_KEY: '123456789',
RUDDERSTACK_URL: 'http://example.com',
};

rudderstack = new RudderStack();
});

afterAll(() => {
process.env = originalEnv;
});

test('should be initialized when instance is created', () => {
expect(rudderstack.has_initialized).toBe(true);
});

test('should be identified once identify event is called', () => {
rudderstack.identifyEvent('C123', {
language: 'en',
});

expect(rudderstack.has_identified).toBe(true);
});

test('should not be empty if current page is passed', () => {
rudderstack.pageView('app.deriv.com');

expect(rudderstack.current_page).not.toBe('');
});

test('should be called once when track is invoked', () => {
const spy = jest.spyOn(rudderstack, 'track');
rudderstack.track('ce_trade_types_form', {
action: 'open',
});

expect(spy).toHaveBeenCalledTimes(1);
});

test('should not be identified when reset is called', () => {
rudderstack.reset();

expect(rudderstack.has_identified).toBe(false);
});
});
1 change: 1 addition & 0 deletions packages/analytics/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as RudderStack } from './rudderstack';
164 changes: 164 additions & 0 deletions packages/analytics/src/rudderstack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import * as RudderAnalytics from 'rudder-sdk-js';

type SignupProvider = 'email' | 'phone' | 'google' | 'facebook' | 'apple';

type VirtualSignupFormAction = {
action:
| 'open'
| 'started'
| 'email_confirmed'
| 'signup_continued'
| 'country_selection_screen_opened'
| 'password_screen_opened'
| 'signup_done'
| 'signup_flow_error'
| 'go_to_login';
signup_provider?: SignupProvider;
form_source?: string;
form_name?: string;
error_message?: string;
};

type RealAccountSignupFormAction = {
action:
| 'open'
| 'step_passed'
| 'save'
| 'restore'
| 'close'
| 'real_signup_error'
| 'other_error'
| 'real_signup_finished';
step_codename?: string;
step_num?: number;
user_choice?: string;
source?: string;
form_name?: string;
real_signup_error_message?: string;
landing_company: string;
};

type VirtualSignupEmailConfirmationAction = {
action: 'received' | 'expired' | 'confirmed' | 'error';
signup_provider?: SignupProvider;
form_source?: string;
email_md5?: string;
error_message?: string;
};

type TradeTypesFormAction =
| {
action: 'open' | 'close' | 'info_close';
trade_type_name?: string;
tab_name?: string;
form_source?: string;
form_name?: string;
subform_name?: string;
}
| {
action: 'choose_trade_type';
subform_name: 'info_old' | 'info_new';
form_name: string;
trade_type_name: string;
}
| {
action: 'choose_trade_type';
subform_name: 'trade_type';
tab_name: string;
form_name: string;
trade_type_name: string;
}
| {
action: 'search';
search_string: string;
}
| {
action: 'info_open';
tab_name: string;
trade_type_name: string;
}
| {
action: 'info-switcher';
info_switcher_mode: string;
trade_type_name: string;
};

type IdentifyAction = {
language: string;
};

type TEvents = {
ce_virtual_signup_form: VirtualSignupFormAction;
ce_real_account_signup_form: RealAccountSignupFormAction;
ce_virtual_signup_email_confirmation: VirtualSignupEmailConfirmationAction;
ce_trade_types_form: TradeTypesFormAction;
identify: IdentifyAction;
};

export class RudderStack {
has_identified = false;
has_initialized = false;
current_page = '';

constructor() {
this.init();
}

init() {
const isProduction = process.env.CIRCLE_JOB === 'release_production';
const isStaging = process.env.CIRCLE_JOB === 'release_staging';

let RUDDERSTACK_KEY;
if (isProduction) {
RUDDERSTACK_KEY = process.env.RUDDERSTACK_PRODUCTION_KEY;
} else if (isStaging) {
RUDDERSTACK_KEY = process.env.RUDDERSTACK_STAGING_KEY;
}

const RUDDERSTACK_URL = process.env.RUDDERSTACK_URL;
if (RUDDERSTACK_KEY && RUDDERSTACK_URL) {
RudderAnalytics.load(RUDDERSTACK_KEY, RUDDERSTACK_URL);
RudderAnalytics.ready(() => {
this.has_initialized = true;
});
}
}

identifyEvent = (user_id: string, payload: TEvents['identify']) => {
if (this.has_initialized) {
RudderAnalytics.identify(user_id, payload);
this.has_identified = true;
}
};

/**
* Pushes page view track event to rudderstack
*/
pageView(current_page: string) {
if (this.has_initialized && this.has_identified && current_page !== this.current_page) {
RudderAnalytics.page('Deriv App', current_page);
this.current_page = current_page;
}
}

/**
* Pushes reset event to rudderstack
*/
reset() {
if (this.has_initialized) {
RudderAnalytics.reset();
this.has_identified = false;
}
}

/**
* Pushes track events to rudderstack
*/
track<T extends keyof TEvents>(event: T, payload: TEvents[T]) {
if (this.has_initialized && this.has_identified) {
RudderAnalytics.track(event, payload);
}
}
}

export default new RudderStack();
4 changes: 4 additions & 0 deletions packages/analytics/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src"]
}
1 change: 0 additions & 1 deletion packages/cfd/src/Stores/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export default class RootStore {
this.modules = new ModulesStore(this, core_store);
this.ui = core_store.ui;
this.gtm = core_store.gtm;
this.rudderstack = core_store.rudderstack;
this.pushwoosh = core_store.pushwoosh;
this.notifications = core_store.notifications;
this.traders_hub = core_store.traders_hub;
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"dependencies": {
"@babel/polyfill": "^7.4.4",
"@deriv/account": "^1.0.0",
"@deriv/analytics": "^1.0.0",
"@deriv/api": "^1.0.0",
"@deriv/appstore": "^0.0.4",
"@deriv/bot-web-ui": "^1.0.0",
Expand Down
25 changes: 16 additions & 9 deletions packages/core/src/App/Containers/Layout/app-contents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { withRouter } from 'react-router';
import WS from 'Services/ws-methods';
import { DesktopWrapper, MobileWrapper, ThemedScrollbars } from '@deriv/components';
import { CookieStorage, isMobile, TRACKING_STATUS_KEY, PlatformContext, platforms, routes } from '@deriv/shared';
import { RudderStack } from '@deriv/analytics';
import { connect } from 'Stores/connect';
import CookieBanner from '../../Components/Elements/CookieBanner/cookie-banner.jsx';
import { useStore } from '@deriv/stores';
import { getLanguage } from '@deriv/translations';

const tracking_status_cookie = new CookieStorage(TRACKING_STATUS_KEY);

const AppContents = ({
children,
identifyEvent,
is_app_disabled,
is_cashier_visible,
is_dark_mode,
Expand All @@ -24,19 +26,30 @@ const AppContents = ({
is_route_modal_on,
notifyAppInstall,
platform,
pageView,
pushDataLayer,
setAppContentsScrollRef,
}) => {
const [show_cookie_banner, setShowCookieBanner] = React.useState(false);
const [is_gtm_tracking, setIsGtmTracking] = React.useState(false);
const { is_appstore } = React.useContext(PlatformContext);
const {
client: { user_id },
} = useStore();

const tracking_status = tracking_status_cookie.get(TRACKING_STATUS_KEY);

const scroll_ref = React.useRef(null);

React.useEffect(() => {
// rudderstack page view trigger
WS.wait('authorize').then(() => {
RudderStack.identifyEvent(user_id, {
language: getLanguage().toLowerCase() || 'en',
});
const current_page = window.location.hostname + window.location.pathname;
RudderStack.pageView(current_page);
});

if (scroll_ref.current) setAppContentsScrollRef(scroll_ref);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand All @@ -57,10 +70,6 @@ const AppContents = ({
}
}, [tracking_status, is_logged_in, is_eu_country, is_logging_in]);

// rudderstack page view trigger
identifyEvent();
pageView();

React.useEffect(() => {
const handleInstallPrompt = e => {
// Prevent the mini-infobar from appearing on mobile
Expand Down Expand Up @@ -145,14 +154,12 @@ AppContents.propTypes = {
};

export default withRouter(
connect(({ client, common, gtm, rudderstack, ui }) => ({
connect(({ client, common, gtm, ui }) => ({
is_eu_country: client.is_eu_country,
is_eu: client.is_eu,
is_logged_in: client.is_logged_in,
is_logging_in: client.is_logging_in,
pushDataLayer: gtm.pushDataLayer,
identifyEvent: rudderstack.identifyEvent,
pageView: rudderstack.pageView,
is_app_disabled: ui.is_app_disabled,
is_cashier_visible: ui.is_cashier_visible,
is_dark_mode: ui.is_dark_mode_on,
Expand Down
Loading

0 comments on commit 37b843d

Please sign in to comment.