fix(client): GA to GTM Migration (#48811)

This commit is contained in:
Ahmad Abdolsaheb
2023-01-07 09:06:45 +03:00
committed by GitHub
parent 0a20f8fd73
commit 8b5838ef23
28 changed files with 138 additions and 294 deletions
+2 -1
View File
@@ -97,7 +97,7 @@
"react": "16.14.0",
"react-dom": "16.14.0",
"react-final-form": "6.5.9",
"react-ga": "3.3.1",
"react-gtm-module": "2.0.11",
"react-helmet": "6.1.0",
"react-hotkeys": "2.0.0",
"react-i18next": "11.18.6",
@@ -138,6 +138,7 @@
"@faker-js/faker": "7.6.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@types/react-gtm-module": "2.0.1",
"autoprefixer": "10.4.13",
"babel-plugin-transform-imports": "2.0.0",
"chokidar": "3.5.3",
+18
View File
@@ -0,0 +1,18 @@
import TagManager from 'react-gtm-module';
import {
devAnalyticsId,
prodAnalyticsId
} from '../../../config/analytics-settings';
import envData from '../../../config/env.json';
const { deploymentEnv } = envData;
const gtmId = deploymentEnv === 'staging' ? devAnalyticsId : prodAnalyticsId;
if (typeof document !== `undefined`) {
TagManager.initialize({ gtmId });
}
export default TagManager;
-15
View File
@@ -1,15 +0,0 @@
import ReactGA from 'react-ga';
import {
devAnalyticsId,
prodAnalyticsId
} from '../../../config/analytics-settings';
import envData from '../../../config/env.json';
const { deploymentEnv } = envData;
const analyticsId =
deploymentEnv === 'staging' ? devAnalyticsId : prodAnalyticsId;
ReactGA.initialize(analyticsId);
export default ReactGA;
@@ -155,12 +155,8 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
) {
setIsDonationDisplayed(true);
executeGA({
type: 'event',
data: {
category: 'Donation View',
action: 'Displayed Certificate Donation',
nonInteraction: true
}
event: 'donationview',
action: 'Displayed Certificate Donation'
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
+4 -11
View File
@@ -19,8 +19,7 @@ import {
userSelector,
isDonatingSelector,
signInLoadingSelector,
donationFormStateSelector,
isVariantASelector
donationFormStateSelector
} from '../../redux/selectors';
import Spacer from '../helpers/spacer';
import { Themes } from '../settings/theme';
@@ -87,7 +86,6 @@ type DonateFormProps = {
) => string;
theme: Themes;
updateDonationFormState: (state: DonationApprovalData) => unknown;
isVariantA: boolean;
paymentContext: PaymentContext;
};
@@ -97,22 +95,19 @@ const mapStateToProps = createSelector(
isDonatingSelector,
donationFormStateSelector,
userSelector,
isVariantASelector,
(
showLoading: DonateFormProps['showLoading'],
isSignedIn: DonateFormProps['isSignedIn'],
isDonating: DonateFormProps['isDonating'],
donationFormState: DonateFormState,
{ email, theme }: { email: string; theme: Themes },
isVariantA: boolean
{ email, theme }: { email: string; theme: Themes }
) => ({
isSignedIn,
isDonating,
showLoading,
donationFormState,
email,
theme,
isVariantA
theme
})
);
@@ -280,8 +275,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
t,
isMinimalForm,
isSignedIn,
isDonating,
isVariantA
isDonating
} = this.props;
const priorityTheme = defaultTheme ? defaultTheme : theme;
const isOneTime = donationDuration === 'one-time';
@@ -335,7 +329,6 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
processing={processing}
t={t}
theme={priorityTheme}
isVariantA={isVariantA}
/>
</>
)}
@@ -66,16 +66,12 @@ function DonateModal({
useEffect(() => {
if (show) {
void playTone('donation');
executeGA({ type: 'modal', data: '/donation-modal' });
executeGA({ event: 'pageview', pagePath: '/donation-modal' });
executeGA({
type: 'event',
data: {
category: 'Donation View',
action: `Displayed ${
recentlyClaimedBlock ? 'block' : 'progress'
} donation modal`,
nonInteraction: true
}
event: 'donationview',
action: `Displayed ${
recentlyClaimedBlock ? 'Block' : 'Progress'
} Donation Modal`
});
}
}, [show, recentlyClaimedBlock, executeGA]);
@@ -17,7 +17,6 @@ import { PaymentProvider } from '../../../../config/donation-settings';
import envData from '../../../../config/env.json';
import { Themes } from '../settings/theme';
import { DonationApprovalData, PostPayment } from './types';
import SecurityLockIcon from './security-lock-icon';
const { stripePublicKey }: { stripePublicKey: string | null } = envData;
@@ -27,7 +26,6 @@ interface FormPropTypes {
t: (label: string) => string;
theme: Themes;
processing: boolean;
isVariantA: boolean;
}
interface Element {
@@ -43,8 +41,7 @@ const StripeCardForm = ({
t,
onDonationStateChange,
postPayment,
processing,
isVariantA
processing
}: FormPropTypes): JSX.Element => {
const [isSubmissionValid, setSubmissionValidity] = useState(true);
const [isTokenizing, setTokenizing] = useState(false);
@@ -166,7 +163,6 @@ const StripeCardForm = ({
disabled={!stripe || !elements || isSubmitting}
type='submit'
>
{!isVariantA && <SecurityLockIcon />}
{t('buttons.donate')}
</Button>
</Form>
@@ -8,7 +8,6 @@ import i18nTestConfig from '../../i18n/config-for-tests';
import { createStore } from '../redux/createStore';
import AppMountNotifier from './app-mount-notifier';
jest.mock('react-ga');
jest.unmock('react-i18next');
type Language = keyof typeof i18nextCodes;
@@ -3,12 +3,11 @@ import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchUser, executeGA } from '../../redux/actions';
import { fetchUser } from '../../redux/actions';
import { isSignedInSelector } from '../../redux/selectors';
interface CertificationProps {
children?: React.ReactNode;
executeGA?: (args: { type: string; data: string }) => void;
fetchUser: () => void;
isSignedIn?: boolean;
pathname: string;
@@ -18,18 +17,15 @@ const mapStateToProps = createSelector(isSignedInSelector, isSignedIn => ({
isSignedIn
}));
const mapDispatchToProps = { fetchUser, executeGA };
const mapDispatchToProps = { fetchUser };
class CertificationLayout extends Component<CertificationProps> {
static displayName = 'CertificationLayout';
componentDidMount() {
const { isSignedIn, fetchUser, pathname } = this.props;
const { isSignedIn, fetchUser } = this.props;
if (!isSignedIn) {
fetchUser();
}
if (this.props.executeGA) {
this.props.executeGA({ type: 'page', data: pathname });
}
}
render(): JSX.Element {
+4 -15
View File
@@ -1,6 +1,7 @@
import React, { Component, ReactNode } from 'react';
import Helmet from 'react-helmet';
import { TFunction, withTranslation } from 'react-i18next';
// import TagManager from 'react-gtm-module';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { createSelector } from 'reselect';
@@ -16,8 +17,7 @@ import { isBrowser } from '../../../utils';
import {
fetchUser,
onlineStatusChange,
serverStatusChange,
executeGA
serverStatusChange
} from '../../redux/actions';
import {
isSignedInSelector,
@@ -76,8 +76,7 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
fetchUser,
removeFlashMessage,
onlineStatusChange,
serverStatusChange,
executeGA
serverStatusChange
},
dispatch
);
@@ -105,24 +104,14 @@ class DefaultLayout extends Component<DefaultLayoutProps> {
static displayName = 'DefaultLayout';
componentDidMount() {
const { isSignedIn, fetchUser, pathname, executeGA } = this.props;
const { isSignedIn, fetchUser } = this.props;
if (!isSignedIn) {
fetchUser();
}
executeGA({ type: 'page', data: pathname });
window.addEventListener('online', this.updateOnlineStatus);
window.addEventListener('offline', this.updateOnlineStatus);
}
componentDidUpdate(prevProps: DefaultLayoutProps) {
const { pathname, executeGA } = this.props;
const { pathname: prevPathname } = prevProps;
if (pathname !== prevPathname) {
executeGA({ type: 'page', data: pathname });
}
}
componentWillUnmount() {
window.removeEventListener('online', this.updateOnlineStatus);
window.removeEventListener('offline', this.updateOnlineStatus);
@@ -7,7 +7,6 @@ import { render, screen } from '../../../../utils/test-utils';
import { createStore } from '../../../redux/createStore';
import TimeLine from './time-line';
jest.mock('react-ga');
const store = createStore();
beforeEach(() => {
+6 -14
View File
@@ -22,14 +22,10 @@ import { signInLoadingSelector, userSelector } from '../redux/selectors';
import { PaymentContext } from '../../../config/donation-settings';
export interface ExecuteGaArg {
type: string;
data: {
category: string;
action: string;
nonInteraction?: boolean;
label?: string;
value?: number;
};
event: string;
action: string;
duration?: string;
amount?: number;
}
interface DonatePageProps {
executeGA: (arg: ExecuteGaArg) => void;
@@ -59,12 +55,8 @@ function DonatePage({
}: DonatePageProps) {
useEffect(() => {
executeGA({
type: 'event',
data: {
category: 'Donation View',
action: `Displayed donate page`,
nonInteraction: true
}
event: 'donationview',
action: `Displayed Donate Page`
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+5 -5
View File
@@ -11,6 +11,7 @@ import Intro from '../components/Intro';
import Map from '../components/Map';
import { Spacer } from '../components/helpers';
import LearnLayout from '../components/layouts/learn';
import { defaultDonation } from '../../../config/donation-settings';
import {
isSignedInSelector,
userSelector,
@@ -82,11 +83,10 @@ function LearnPage({
const onDonationAlertClick = () => {
executeGA({
type: 'event',
data: {
category: 'Donation Related',
action: `learn donation alert click`
}
event: 'donationrelated',
action: `Learn Donation Alert Click`,
duration: defaultDonation.donationDuration,
amount: defaultDonation.donationAmount
});
};
return (
+7 -10
View File
@@ -103,16 +103,13 @@ export function* postChargeSaga({
}
yield put(
executeGA({
type: 'event',
data: {
category:
paymentProvider === PaymentProvider.Patreon
? 'Donation Related'
: 'Donation',
action: stringifyDonationEvents(paymentContext, paymentProvider),
label: duration,
value: amount
}
event:
paymentProvider === PaymentProvider.Patreon
? 'donationrelated'
: 'donation',
action: stringifyDonationEvents(paymentContext, paymentProvider),
duration,
amount
})
);
} catch (error) {
+9 -15
View File
@@ -22,13 +22,10 @@ const postChargeDataMock = {
};
const analyticsDataMock = {
type: 'event',
data: {
category: 'Donation',
action: 'Donate Page Stripe Payment Submission',
label: 'monthly',
value: '500'
}
event: 'donation',
action: 'Donate Page Stripe Payment Submission',
duration: 'monthly',
amount: '500'
};
describe('donation-saga', () => {
@@ -48,7 +45,7 @@ describe('donation-saga', () => {
};
const stripeCardAnalyticsDataMock = analyticsDataMock;
stripeCardAnalyticsDataMock.data.action =
stripeCardAnalyticsDataMock.action =
'Donate Page Stripe Card Payment Submission';
const { paymentMethodId, amount, duration } = stripeCardDataMock.payload;
@@ -68,8 +65,7 @@ describe('donation-saga', () => {
};
const paypalAnalyticsDataMock = analyticsDataMock;
paypalAnalyticsDataMock.data.action =
'Donate Page Paypal Payment Submission';
paypalAnalyticsDataMock.action = 'Donate Page Paypal Payment Submission';
const storeMock = {
app: {
@@ -94,8 +90,7 @@ describe('donation-saga', () => {
};
const paypalAnalyticsDataMock = analyticsDataMock;
paypalAnalyticsDataMock.data.action =
'Donate Page Paypal Payment Submission';
paypalAnalyticsDataMock.action = 'Donate Page Paypal Payment Submission';
const storeMock = {
app: {}
@@ -117,9 +112,8 @@ describe('donation-saga', () => {
};
const patreonAnalyticsDataMock = analyticsDataMock;
patreonAnalyticsDataMock.data.action =
'Donate Page Patreon Payment Redirection';
patreonAnalyticsDataMock.data.category = 'Donation Related';
patreonAnalyticsDataMock.action = 'Donate Page Patreon Payment Redirection';
patreonAnalyticsDataMock.event = 'donationrelated';
return expectSaga(postChargeSaga, patreonDataMock)
.not.call.fn(addDonation)
.not.call.fn(postChargeStripeCard)
+28 -51
View File
@@ -1,57 +1,34 @@
/* eslint-disable camelcase */
import { all, call, select, takeEvery } from 'redux-saga/effects';
import { all, call, takeEvery } from 'redux-saga/effects';
import TagManager from '../analytics';
import { aBTestConfig } from '../../../config/donation-settings';
import ga from '../analytics';
import { emailToABVariant } from '../utils/A-B-tester';
import {
completedChallengesSelector,
completionCountSelector,
emailSelector,
recentlyClaimedBlockSelector
} from './selectors';
const GaTypes = { event: ga.event, page: ga.pageview, modal: ga.modalview };
function* callGaType({ payload: { type, data } }) {
if (
type === 'event' &&
data.category.toLowerCase().includes('donation') &&
aBTestConfig.isTesting
) {
const email = yield select(emailSelector);
// a b test results are only reported when user is signed in and has email
if (email) {
const completedChallengeTotal = yield select(completedChallengesSelector);
const completedChallengeSession = yield select(completionCountSelector);
let viewType = null;
// set the modal type
if (data.action.toLowerCase().includes('modal')) {
const recentlyClaimedBlock = yield select(recentlyClaimedBlockSelector);
viewType = recentlyClaimedBlock ? 'block' : 'progress';
function* callGaType({
payload: { action, duration, amount, event, pagePath }
}) {
if (event === 'pageview') {
yield call(TagManager.dataLayer, {
dataLayer: {
event,
pagePath
}
const customDimensions = {
// URL;
dimension1: window.location.href,
// Challenges_Completed_Session
dimension2: completedChallengeSession,
// Challenges_Completed_Total
dimension3: completedChallengeTotal.length,
// Test_Type
dimension4: aBTestConfig.type,
// Test_Variation
dimension5: emailToABVariant(email).isVariantA ? 'A' : 'B',
// View_Type
dimension6: viewType
};
ga.set(customDimensions);
}
});
} else if (event === 'donationview') {
yield call(TagManager.dataLayer, {
dataLayer: {
event,
action
}
});
} else {
// donation and donationrelated
yield call(TagManager.dataLayer, {
dataLayer: {
event,
action,
duration,
amount
}
});
}
yield call(GaTypes[type], data);
}
export function* createGaSaga(types) {
+6 -9
View File
@@ -1,24 +1,21 @@
import { expectSaga } from 'redux-saga-test-plan';
import ga from '../analytics';
import TagManager from '../analytics';
import { actionTypes } from './action-types';
import { createGaSaga } from './ga-saga';
jest.mock('../analytics');
describe('ga-saga', () => {
it('calls GA after executeGA action', () => {
const GaTypes = { event: ga.event, page: ga.pageview, modal: ga.modalview };
const mockEventPayload = {
type: 'event',
data: {
category: 'Map Challenge Click',
action: '/learn'
}
action: 'Learn Donation Alert Click',
amount: 500,
duration: 'month',
event: 'donationrelated'
};
return (
expectSaga(createGaSaga, actionTypes)
// Assert that the `call` with expected paramater will eventually happen.
.call(GaTypes.event, mockEventPayload.data)
.call(TagManager.dataLayer, { dataLayer: mockEventPayload })
// Dispatch any actions that the saga will `take`.
.dispatch({ type: actionTypes.executeGA, payload: mockEventPayload })
+1 -8
View File
@@ -1,5 +1,4 @@
import { SuperBlocks } from '../../../config/certification-settings';
import { emailToABVariant } from '../utils/A-B-tester';
import { ns as MainApp } from './action-types';
export const savedChallengesSelector = state =>
@@ -13,13 +12,7 @@ export const currentChallengeIdSelector = state =>
state[MainApp].currentChallengeId;
export const emailSelector = state => userSelector(state).email;
export const isVariantASelector = state => {
const email = emailSelector(state);
// if the user is not signed in and the user info is not available.
// always return A the control variant
if (!email) return true;
return emailToABVariant(email).isVariantA;
};
export const isDonatingSelector = state => userSelector(state).isDonating;
export const isOnlineSelector = state => state[MainApp].isOnline;
export const isServerOnlineSelector = state => state[MainApp].isServerOnline;
@@ -48,7 +48,7 @@ function withActions(...fns: Array<() => void>) {
function ResetModal({ reset, close, isOpen }: ResetModalProps): JSX.Element {
const { t } = useTranslation();
if (isOpen) {
executeGA({ type: 'modal', data: '/reset-modal' });
executeGA({ event: 'pageview', pagePath: '/reset-modal' });
}
return (
<Modal
@@ -254,7 +254,7 @@ class CompletionModalInner extends Component<
const totalChallengesInBlock = currentBlockIds?.length ?? 0;
if (isOpen) {
executeGA({ type: 'modal', data: '/completion-modal' });
executeGA({ event: 'pageview', pagePath: '/completion-modal' });
}
// normally dashedName should be graphQL queried and then passed around,
// but it's only used to make a nice filename for downloading, so dasherize
@@ -16,7 +16,7 @@ import './help-modal.css';
interface HelpModalProps {
closeHelpModal: () => void;
createQuestion: () => void;
executeGA: (attributes: { type: string; data: string }) => void;
executeGA: (attributes: { event: string; pagePath: string }) => void;
isOpen?: boolean;
t: (text: string) => string;
challengeTitle: string;
@@ -54,7 +54,7 @@ function HelpModal({
challengeTitle
}: HelpModalProps): JSX.Element {
if (isOpen) {
executeGA({ type: 'modal', data: '/help-modal' });
executeGA({ event: 'pageview', pagePath: '/help-modal' });
}
return (
<Modal dialogClassName='help-modal' onHide={closeHelpModal} show={isOpen}>
@@ -12,7 +12,7 @@ import './video-modal.css';
interface VideoModalProps {
closeVideoModal: () => void;
executeGA: (attributes: { type: string; data: string }) => void;
executeGA: (attributes: { event: string; pagePath: string }) => void;
isOpen?: boolean;
t: (attribute: string) => string;
videoUrl?: string;
@@ -36,7 +36,7 @@ function VideoModal({
videoUrl
}: VideoModalProps): JSX.Element {
if (isOpen) {
executeGA({ type: 'modal', data: '/completion-modal' });
executeGA({ event: 'pageview', pagePath: '/completion-modal' });
}
return (
<Modal dialogClassName='video-modal' onHide={closeVideoModal} show={isOpen}>
@@ -13,7 +13,6 @@ import DropDown from '../../../assets/icons/dropdown';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { Link, Spacer } from '../../../components/helpers';
import { executeGA } from '../../../redux/actions';
import { completedChallengesSelector } from '../../../redux/selectors';
import { ChallengeNode, CompletedChallenge } from '../../../redux/prop-types';
import { playTone } from '../../../utils/tone';
@@ -45,13 +44,12 @@ const mapStateToProps = (
};
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators({ toggleBlock, executeGA }, dispatch);
bindActionCreators({ toggleBlock }, dispatch);
interface BlockProps {
blockDashedName: string;
challenges: ChallengeNode[];
completedChallengeIds: string[];
executeGA: typeof executeGA;
isExpanded: boolean;
superBlock: SuperBlocks;
t: TFunction;
@@ -66,15 +64,8 @@ class Block extends Component<BlockProps> {
}
handleBlockClick(): void {
const { blockDashedName, toggleBlock, executeGA } = this.props;
const { blockDashedName, toggleBlock } = this.props;
void playTone('block-toggle');
executeGA({
type: 'event',
data: {
category: 'Map Block Click',
action: blockDashedName
}
});
toggleBlock(blockDashedName);
}
@@ -9,7 +9,6 @@ import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { executeGA } from '../../../redux/actions';
import { SuperBlocks } from '../../../../../config/certification-settings';
import { ExecuteGaArg } from '../../../pages/donate';
import { ChallengeWithCompletedNode } from '../../../redux/prop-types';
import { isNewJsCert, isNewRespCert } from '../../../utils/is-a-cert';
@@ -18,7 +17,6 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
interface Challenges {
challengesWithCompleted: ChallengeWithCompletedNode[];
executeGA: (payload: ExecuteGaArg) => void;
isProjectBlock: boolean;
superBlock: SuperBlocks;
blockTitle?: string | null;
@@ -26,20 +24,11 @@ interface Challenges {
function Challenges({
challengesWithCompleted,
executeGA,
isProjectBlock,
superBlock,
blockTitle
}: Challenges): JSX.Element {
const { t } = useTranslation();
const handleChallengeClick = (slug: string) =>
executeGA({
type: 'event',
data: {
category: 'Map Challenge Click',
action: slug
}
});
const renderCheckMark = (isCompleted: boolean) =>
isCompleted ? <GreenPass /> : <GreenNotCompleted />;
@@ -60,9 +49,6 @@ function Challenges({
<div className='challenge-jump-link'>
<Link
className='btn btn-primary'
onClick={() =>
handleChallengeClick(firstIncompleteChallenge.fields.slug)
}
to={firstIncompleteChallenge.fields.slug}
>
{!isChallengeStarted
@@ -88,7 +74,6 @@ function Challenges({
>
{!isProjectBlock ? (
<Link
onClick={() => handleChallengeClick(challenge.fields.slug)}
to={challenge.fields.slug}
className={`map-grid-item ${
+challenge.isCompleted ? 'challenge-completed' : ''
@@ -103,10 +88,7 @@ function Challenges({
</span>
</Link>
) : (
<Link
onClick={() => handleChallengeClick(challenge.fields.slug)}
to={challenge.fields.slug}
>
<Link to={challenge.fields.slug}>
{challenge.title}
<span className=' badge map-badge map-project-checkmark'>
{renderCheckMark(challenge.isCompleted)}
@@ -129,20 +111,14 @@ function Challenges({
key={'map-challenge' + challenge.fields.slug}
>
{!isProjectBlock ? (
<Link
onClick={() => handleChallengeClick(challenge.fields.slug)}
to={challenge.fields.slug}
>
<Link to={challenge.fields.slug}>
<span className='badge map-badge'>
{renderCheckMark(challenge.isCompleted)}
</span>
{challenge.title}
</Link>
) : (
<Link
onClick={() => handleChallengeClick(challenge.fields.slug)}
to={challenge.fields.slug}
>
<Link to={challenge.fields.slug}>
{challenge.title}
<span className='badge map-badge map-project-checkmark'>
{renderCheckMark(challenge.isCompleted)}
-32
View File
@@ -1,32 +0,0 @@
import { faker } from '@faker-js/faker';
import { emailToABVariant } from './A-B-tester';
describe('client/src is-email-variation-a', () => {
it('Consistently returns the same result for the same input', () => {
const preSavedResult = {
hash: '23e3cacb302b0c759531faa8b414b23709c26029',
isVariantA: true,
hashInt: 2
};
const result = emailToABVariant('foo@freecodecamp.org');
expect(result).toEqual(preSavedResult);
});
it('Distributes A and B variations equaly for 100K random emails', () => {
let A = 0;
let B = 0;
const sampleSize = 100000;
faker.seed(123);
for (let i = 0; i < sampleSize; i++) {
if (emailToABVariant(faker.internet.email()).isVariantA) A++;
else B++;
}
const isBucketWellDistributed = (variant: number): boolean =>
variant > 0.99 * (sampleSize / 2);
expect(isBucketWellDistributed(A) && isBucketWellDistributed(B)).toEqual(
true
);
});
});
-21
View File
@@ -1,21 +0,0 @@
import sha1 from 'sha-1';
// This function turns an email to a hash and decides if it should be
// an A or B variant for A/B testing
export function emailToABVariant(email: string): {
hash: string;
isVariantA: boolean;
hashInt: number;
} {
// turn the email into a number
const hash: string = sha1(email);
const hashInt = parseInt(hash.slice(0, 1), 16);
// turn the number to A or B variant
const isVariantA = hashInt % 2 === 0;
return {
hash,
isVariantA,
hashInt
};
}
+2 -2
View File
@@ -1,2 +1,2 @@
exports.prodAnalyticsId = 'UA-55446531-10';
exports.devAnalyticsId = 'UA-55446531-19';
exports.prodAnalyticsId = 'GTM-57R6KJM';
exports.devAnalyticsId = 'GTM-WSS47LM';
+24 -12
View File
@@ -487,7 +487,7 @@
"react": "16.14.0",
"react-dom": "16.14.0",
"react-final-form": "6.5.9",
"react-ga": "3.3.1",
"react-gtm-module": "2.0.11",
"react-helmet": "6.1.0",
"react-hotkeys": "2.0.0",
"react-i18next": "11.18.6",
@@ -528,6 +528,7 @@
"@faker-js/faker": "7.6.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@types/react-gtm-module": "2.0.1",
"autoprefixer": "10.4.13",
"babel-plugin-transform-imports": "2.0.0",
"chokidar": "3.5.3",
@@ -14180,6 +14181,12 @@
"@types/react": "^17"
}
},
"node_modules/@types/react-gtm-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/react-gtm-module/-/react-gtm-module-2.0.1.tgz",
"integrity": "sha512-T/DN9gAbCYk5wJ1nxf4pSwmXz4d1iVjM++OoG+mwMfz9STMAotGjSb65gJHOS5bPvl6vLSsJnuC+y/43OQrltg==",
"dev": true
},
"node_modules/@types/react-helmet": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.6.tgz",
@@ -43031,13 +43038,10 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-ga": {
"version": "3.3.1",
"license": "Apache-2.0",
"peerDependencies": {
"prop-types": "^15.6.0",
"react": "^15.6.2 || ^16.0 || ^17 || ^18"
}
"node_modules/react-gtm-module": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/react-gtm-module/-/react-gtm-module-2.0.11.tgz",
"integrity": "sha512-8gyj4TTxeP7eEyc2QKawEuQoAZdjKvMY4pgWfycGmqGByhs17fR+zEBs0JUDq4US/l+vbTl+6zvUIx27iDo/Vw=="
},
"node_modules/react-helmet": {
"version": "6.1.0",
@@ -56463,6 +56467,7 @@
"@stripe/stripe-js": "1.46.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@types/react-gtm-module": "2.0.1",
"@types/react-scrollable-anchor": "0.6.1",
"algoliasearch": "4.14.3",
"assert": "2.0.0",
@@ -56514,7 +56519,7 @@
"react": "16.14.0",
"react-dom": "16.14.0",
"react-final-form": "6.5.9",
"react-ga": "3.3.1",
"react-gtm-module": "2.0.11",
"react-helmet": "6.1.0",
"react-hotkeys": "2.0.0",
"react-i18next": "11.18.6",
@@ -64541,6 +64546,12 @@
"@types/react": "^17"
}
},
"@types/react-gtm-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/react-gtm-module/-/react-gtm-module-2.0.1.tgz",
"integrity": "sha512-T/DN9gAbCYk5wJ1nxf4pSwmXz4d1iVjM++OoG+mwMfz9STMAotGjSb65gJHOS5bPvl6vLSsJnuC+y/43OQrltg==",
"dev": true
},
"@types/react-helmet": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.6.tgz",
@@ -83621,9 +83632,10 @@
"@babel/runtime": "^7.15.4"
}
},
"react-ga": {
"version": "3.3.1",
"requires": {}
"react-gtm-module": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/react-gtm-module/-/react-gtm-module-2.0.11.tgz",
"integrity": "sha512-8gyj4TTxeP7eEyc2QKawEuQoAZdjKvMY4pgWfycGmqGByhs17fR+zEBs0JUDq4US/l+vbTl+6zvUIx27iDo/Vw=="
},
"react-helmet": {
"version": "6.1.0",