web/satellite: add AB test for new upgrade banner
This change adds AB testing for a new upgrade banner and sends a hit event when "Upgrade" is clicked. Change-Id: Ie463e224af5b0cb74a601b68eedb2b34f9089fd7
This commit is contained in:
parent
11e229cc7f
commit
4edef9e05c
@ -44,6 +44,7 @@
|
|||||||
<meta name="native-token-payments-enabled" content="{{ .NativeTokenPaymentsEnabled }}">
|
<meta name="native-token-payments-enabled" content="{{ .NativeTokenPaymentsEnabled }}">
|
||||||
<meta name="password-minimum-length" content="{{ .PasswordMinimumLength }}">
|
<meta name="password-minimum-length" content="{{ .PasswordMinimumLength }}">
|
||||||
<meta name="password-maximum-length" content="{{ .PasswordMaximumLength }}">
|
<meta name="password-maximum-length" content="{{ .PasswordMaximumLength }}">
|
||||||
|
<meta name="ab-testing-enabled" content="{{ .ABTestingEnabled }}">
|
||||||
<title>{{ .SatelliteName }}</title>
|
<title>{{ .SatelliteName }}</title>
|
||||||
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACDVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////8nbP8obf8pbf8qbv8rb/8sb/8tcP8ucf8vcf8vcv8xc/8zdP81df81dv82dv83d/84eP85eP86ef87ev89e/8+fP8/fP9Aff9Bfv9Cfv9Df/9EgP9FgP9Ggf9Hgv9Jg/9LhP9Mhf9Nhv9Oh/9Ph/9RiP9Rif9Sif9Ui/9Vi/9WjP9Xjf9Yjf9aj/9dkf9ekf9ilP9jlf9llv9nl/9omP9rmv9sm/9tnP9vnf9wnv9yn/91of92ov93o/94o/98pv9/qP+Bqf+Cqv+Eq/+FrP+Hrf+Irv+Jr/+Kr/+Msf+Nsv+Stf+Ttf+Ttv+Utv+Vt/+WuP+XuP+Zuf+Zuv+hv/+kwf+lwv+mwv+nw/+oxP+pxP+pxf+qxf+rxv+yy/+0zP+1zf+3zv+4z/+60P+70f+90v+/0/+/1P/B1f/D1v/E1//F1//F2P/G2P/H2f/I2v/J2v/K2//L3P/P3v/Q3//R4P/S4P/V4v/V4//W4//X5P/Y5P/b5v/b5//c5//d6P/e6f/f6f/g6v/h6//j7P/k7f/l7f/m7v/q8f/r8f/u8//w9f/x9f/x9v/z9//0+P/2+f/3+f/3+v/4+v/5+//6/P/8/f/9/v/+/v////9uCbVDAAAAFXRSTlMABAU4Ozw9PpSWl5ilp6ip4+Tl/P6nIcp/AAAAAWJLR0SuuWuTpwAAAh5JREFUOMtjYGBgYOcXEl6HAYSF+FgZQICJex1OwMkEVIAi3+Xh1ozM5wKaj8xfpBwcITsbWYSNgR+JtzpJYvU6jbAVSEK8DEIITpOZqnxItISWfgVCTJAB7v4ZXpKRC9uMNCqXJci6TID7hQFMrV2zJE7abTKQFesDJGb7SYTOX7sGLAVWUKCgrGZcDeaDFaxb12alqC6XDlMwTyKnRLJ1HbKCddNEc0skJkAVdEssXatRiKqgVmLlatUqqILVpuaOEnLJy4GsIhONuHlAOldVwtJWcwnMDb2i4dPKdHVKV3uqRCdYqU9psVDOmh0vUQN35FTRhevWLU+V0FeZBdTtpSQRvgAoKtuMqmBdpKxvKYjXJ+o+cx0WBRPFO6ABHuesMheLghIdePiutc7AoqBLchZchVMSFgUr9HTS8sEgL1C0E1XBRNGUeeV6OlFONjbqSjY2Nv7mKjnzMyXqYQrW2OsYS8smLkOE5OpsFSkdQ6PlUAU9EgtXq6MFdZ3EkpVKNVAFc8TKW6QbURVMFK1slOyGuSFdUkLOoQtZwSRXaRmpKLgj1y1eMjdIImguTMHCCAnvGcuXQhIMPMl1O8hnrOy31GtfnaNi3oRIcohEu7ZY20DZK0DGTCV7NVKi5UVK40vDJVatU/dfgCTEw8AsgsSdLx+TKjUdOeMAsycnMr/BzrIcmc8ByrycuDMvByM4f7PyCmLL/gK8LEBJALYsGEdXEyupAAAAAElFTkSuQmCC" type="image/x-icon">
|
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACDVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////8nbP8obf8pbf8qbv8rb/8sb/8tcP8ucf8vcf8vcv8xc/8zdP81df81dv82dv83d/84eP85eP86ef87ev89e/8+fP8/fP9Aff9Bfv9Cfv9Df/9EgP9FgP9Ggf9Hgv9Jg/9LhP9Mhf9Nhv9Oh/9Ph/9RiP9Rif9Sif9Ui/9Vi/9WjP9Xjf9Yjf9aj/9dkf9ekf9ilP9jlf9llv9nl/9omP9rmv9sm/9tnP9vnf9wnv9yn/91of92ov93o/94o/98pv9/qP+Bqf+Cqv+Eq/+FrP+Hrf+Irv+Jr/+Kr/+Msf+Nsv+Stf+Ttf+Ttv+Utv+Vt/+WuP+XuP+Zuf+Zuv+hv/+kwf+lwv+mwv+nw/+oxP+pxP+pxf+qxf+rxv+yy/+0zP+1zf+3zv+4z/+60P+70f+90v+/0/+/1P/B1f/D1v/E1//F1//F2P/G2P/H2f/I2v/J2v/K2//L3P/P3v/Q3//R4P/S4P/V4v/V4//W4//X5P/Y5P/b5v/b5//c5//d6P/e6f/f6f/g6v/h6//j7P/k7f/l7f/m7v/q8f/r8f/u8//w9f/x9f/x9v/z9//0+P/2+f/3+f/3+v/4+v/5+//6/P/8/f/9/v/+/v////9uCbVDAAAAFXRSTlMABAU4Ozw9PpSWl5ilp6ip4+Tl/P6nIcp/AAAAAWJLR0SuuWuTpwAAAh5JREFUOMtjYGBgYOcXEl6HAYSF+FgZQICJex1OwMkEVIAi3+Xh1ozM5wKaj8xfpBwcITsbWYSNgR+JtzpJYvU6jbAVSEK8DEIITpOZqnxItISWfgVCTJAB7v4ZXpKRC9uMNCqXJci6TID7hQFMrV2zJE7abTKQFesDJGb7SYTOX7sGLAVWUKCgrGZcDeaDFaxb12alqC6XDlMwTyKnRLJ1HbKCddNEc0skJkAVdEssXatRiKqgVmLlatUqqILVpuaOEnLJy4GsIhONuHlAOldVwtJWcwnMDb2i4dPKdHVKV3uqRCdYqU9psVDOmh0vUQN35FTRhevWLU+V0FeZBdTtpSQRvgAoKtuMqmBdpKxvKYjXJ+o+cx0WBRPFO6ABHuesMheLghIdePiutc7AoqBLchZchVMSFgUr9HTS8sEgL1C0E1XBRNGUeeV6OlFONjbqSjY2Nv7mKjnzMyXqYQrW2OsYS8smLkOE5OpsFSkdQ6PlUAU9EgtXq6MFdZ3EkpVKNVAFc8TKW6QbURVMFK1slOyGuSFdUkLOoQtZwSRXaRmpKLgj1y1eMjdIImguTMHCCAnvGcuXQhIMPMl1O8hnrOy31GtfnaNi3oRIcohEu7ZY20DZK0DGTCV7NVKi5UVK40vDJVatU/dfgCTEw8AsgsSdLx+TKjUdOeMAsycnMr/BzrIcmc8ByrycuDMvByM4f7PyCmLL/gK8LEBJALYsGEdXEyupAAAAAElFTkSuQmCC" type="image/x-icon">
|
||||||
<link rel="dns-prefetch" href="https://js.stripe.com">
|
<link rel="dns-prefetch" href="https://js.stripe.com">
|
||||||
|
44
web/satellite/src/api/abtesting.ts
Normal file
44
web/satellite/src/api/abtesting.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (C) 2022 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
|
||||||
|
import { HttpClient } from '@/utils/httpClient';
|
||||||
|
import { ABHitAction, ABTestApi, ABTestValues } from '@/types/abtesting';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ABHttpApi is a console AB testing API.
|
||||||
|
* Exposes all ab-testing related functionality
|
||||||
|
*/
|
||||||
|
export class ABHttpApi implements ABTestApi {
|
||||||
|
private readonly http: HttpClient = new HttpClient();
|
||||||
|
private readonly ROOT_PATH: string = '/api/v0/ab';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to get test banner information.
|
||||||
|
*
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
public async fetchABTestValues(): Promise<ABTestValues> {
|
||||||
|
const path = `${this.ROOT_PATH}/values`;
|
||||||
|
const response = await this.http.get(path);
|
||||||
|
if (response.ok) {
|
||||||
|
const abResponse = await response.json();
|
||||||
|
|
||||||
|
return new ABTestValues(
|
||||||
|
abResponse.has_new_banner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new ErrorUnauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// use default values on error
|
||||||
|
return new ABTestValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendHit(action: ABHitAction): Promise<void> {
|
||||||
|
const path = `${this.ROOT_PATH}/hit/${action}`;
|
||||||
|
await this.http.post(path, null);
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,8 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
|
|||||||
|
|
||||||
import { AnalyticsHttpApi } from '@/api/analytics';
|
import { AnalyticsHttpApi } from '@/api/analytics';
|
||||||
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
||||||
|
import { AB_TESTING_ACTIONS } from '@/store/modules/abTesting';
|
||||||
|
import { ABHitAction } from '@/types/abtesting';
|
||||||
|
|
||||||
// @vue/component
|
// @vue/component
|
||||||
@Component
|
@Component
|
||||||
@ -29,6 +31,7 @@ export default class PaidTierBar extends Vue {
|
|||||||
public async openBanner(): Promise<void> {
|
public async openBanner(): Promise<void> {
|
||||||
this.openAddPMModal();
|
this.openAddPMModal();
|
||||||
await this.analytics.eventTriggered(AnalyticsEvent.UPGRADE_BANNER_CLICKED);
|
await this.analytics.eventTriggered(AnalyticsEvent.UPGRADE_BANNER_CLICKED);
|
||||||
|
await this.$store.dispatch(AB_TESTING_ACTIONS.HIT, ABHitAction.UPGRADE_ACCOUNT_CLICKED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -65,6 +65,7 @@ import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
|||||||
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
|
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
|
||||||
import { MetaUtils } from '@/utils/meta';
|
import { MetaUtils } from '@/utils/meta';
|
||||||
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
|
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
|
||||||
|
import { AB_TESTING_ACTIONS } from '@/store/modules/abTesting';
|
||||||
|
|
||||||
import BillingIcon from '@/../static/images/navigation/billing.svg';
|
import BillingIcon from '@/../static/images/navigation/billing.svg';
|
||||||
import InfoIcon from '@/../static/images/navigation/info.svg';
|
import InfoIcon from '@/../static/images/navigation/info.svg';
|
||||||
@ -152,6 +153,7 @@ export default class AccountArea extends Vue {
|
|||||||
await this.$store.dispatch(OBJECTS_ACTIONS.CLEAR);
|
await this.$store.dispatch(OBJECTS_ACTIONS.CLEAR);
|
||||||
await this.$store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
|
await this.$store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
|
||||||
await this.$store.dispatch(PAYMENTS_ACTIONS.CLEAR_PAYMENT_INFO);
|
await this.$store.dispatch(PAYMENTS_ACTIONS.CLEAR_PAYMENT_INFO);
|
||||||
|
await this.$store.dispatch(AB_TESTING_ACTIONS.RESET);
|
||||||
|
|
||||||
LocalData.removeUserId();
|
LocalData.removeUserId();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
// Copyright (C) 2022 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="isBannerShowing" class="notification-wrap">
|
||||||
|
<div class="notification-wrap__content">
|
||||||
|
<div class="notification-wrap__content__left">
|
||||||
|
<SunnyIcon class="notification-wrap__content__left__icon" />
|
||||||
|
<p>Ready to upgrade? Upload up to 75TB and pay what you use only, no minimum. 150GB free included.</p>
|
||||||
|
</div>
|
||||||
|
<div class="notification-wrap__content__right">
|
||||||
|
<a @click="openBanner">Upgrade Now</a>
|
||||||
|
<CloseIcon class="notification-wrap__content__right__close" @click="onCloseClick" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
|
||||||
|
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
||||||
|
import { AnalyticsHttpApi } from '@/api/analytics';
|
||||||
|
import { AB_TESTING_ACTIONS } from '@/store/modules/abTesting';
|
||||||
|
import { ABHitAction } from '@/types/abtesting';
|
||||||
|
|
||||||
|
import SunnyIcon from '@/../static/images/notifications/sunnyicon.svg';
|
||||||
|
import CloseIcon from '@/../static/images/notifications/closeSmall.svg';
|
||||||
|
|
||||||
|
// @vue/component
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
CloseIcon,
|
||||||
|
SunnyIcon,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class UpgradeNotification extends Vue {
|
||||||
|
@Prop({ default: () => () => false })
|
||||||
|
public readonly openAddPMModal: () => void;
|
||||||
|
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
|
||||||
|
|
||||||
|
public isBannerShowing = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes notification.
|
||||||
|
*/
|
||||||
|
public onCloseClick(): void {
|
||||||
|
this.isBannerShowing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send analytics event to segment when Upgrade Account banner is clicked.
|
||||||
|
public async openBanner(): Promise<void> {
|
||||||
|
this.openAddPMModal();
|
||||||
|
await this.analytics.eventTriggered(AnalyticsEvent.UPGRADE_BANNER_CLICKED);
|
||||||
|
await this.$store.dispatch(AB_TESTING_ACTIONS.HIT, ABHitAction.UPGRADE_ACCOUNT_CLICKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.notification-wrap {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
position: absolute;
|
||||||
|
left: 40px;
|
||||||
|
right: 44px;
|
||||||
|
top: 5px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: 'font_regular', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #d8dee3;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 7px 20px rgba(0 0 0 / 15%);
|
||||||
|
|
||||||
|
&__left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& b {
|
||||||
|
font-family: 'font_medium', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 16px;
|
||||||
|
|
||||||
|
& a {
|
||||||
|
color: black;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close {
|
||||||
|
margin-left: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -24,6 +24,8 @@ import { makeUsersModule } from '@/store/modules/users';
|
|||||||
import { User } from '@/types/users';
|
import { User } from '@/types/users';
|
||||||
import { FilesState, makeFilesModule } from '@/store/modules/files';
|
import { FilesState, makeFilesModule } from '@/store/modules/files';
|
||||||
import { NavigationLink } from '@/types/navigation';
|
import { NavigationLink } from '@/types/navigation';
|
||||||
|
import { ABTestingState, makeABTestingModule } from '@/store/modules/abTesting';
|
||||||
|
import { ABHttpApi } from '@/api/abtesting';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
@ -34,6 +36,7 @@ const bucketsApi = new BucketsApiGql();
|
|||||||
const projectMembersApi = new ProjectMembersApiGql();
|
const projectMembersApi = new ProjectMembersApiGql();
|
||||||
const projectsApi = new ProjectsApiGql();
|
const projectsApi = new ProjectsApiGql();
|
||||||
const paymentsApi = new PaymentsHttpApi();
|
const paymentsApi = new PaymentsHttpApi();
|
||||||
|
const abTestingAPI = new ABHttpApi();
|
||||||
|
|
||||||
// We need to use a WebWorker facotory because jest testing does not support
|
// We need to use a WebWorker facotory because jest testing does not support
|
||||||
// WebWorkers yet. This is a way to avoid a direct dependency to `new Worker`.
|
// WebWorkers yet. This is a way to avoid a direct dependency to `new Worker`.
|
||||||
@ -53,6 +56,7 @@ export interface ModulesState {
|
|||||||
projectsModule: ProjectsState;
|
projectsModule: ProjectsState;
|
||||||
objectsModule: ObjectsState;
|
objectsModule: ObjectsState;
|
||||||
files: FilesState;
|
files: FilesState;
|
||||||
|
abTestingModule: ABTestingState;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Satellite store (vuex)
|
// Satellite store (vuex)
|
||||||
@ -68,6 +72,7 @@ export const store = new Vuex.Store<ModulesState>({
|
|||||||
bucketUsageModule: makeBucketsModule(bucketsApi),
|
bucketUsageModule: makeBucketsModule(bucketsApi),
|
||||||
objectsModule: makeObjectsModule(),
|
objectsModule: makeObjectsModule(),
|
||||||
files: makeFilesModule(),
|
files: makeFilesModule(),
|
||||||
|
abTestingModule: makeABTestingModule(abTestingAPI),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
81
web/satellite/src/store/modules/abTesting.ts
Normal file
81
web/satellite/src/store/modules/abTesting.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (C) 2022 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
import { StoreModule } from '@/types/store';
|
||||||
|
import { MetaUtils } from '@/utils/meta';
|
||||||
|
import { ABHitAction, ABTestApi, ABTestValues } from '@/types/abtesting';
|
||||||
|
|
||||||
|
export const AB_TESTING_ACTIONS = {
|
||||||
|
FETCH: 'fetchAbTestValues',
|
||||||
|
RESET: 'resetAbTestValues',
|
||||||
|
HIT: 'sendHitEvent',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AB_TESTING_MUTATIONS = {
|
||||||
|
SET: 'SET_AB_VALUES',
|
||||||
|
INIT: 'SET_AB_INITIALIZED',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ABTestingState {
|
||||||
|
public abTestValues = new ABTestValues();
|
||||||
|
public abTestingEnabled = MetaUtils.getMetaContent('ab-testing-enabled') === 'true';
|
||||||
|
public abTestingInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ABTestingContext {
|
||||||
|
state: ABTestingState
|
||||||
|
commit: (string, ...unknown) => void
|
||||||
|
dispatch: (string, ...unknown) => Promise<any> // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
FETCH,
|
||||||
|
RESET,
|
||||||
|
HIT,
|
||||||
|
} = AB_TESTING_ACTIONS;
|
||||||
|
|
||||||
|
const {
|
||||||
|
SET,
|
||||||
|
INIT,
|
||||||
|
} = AB_TESTING_MUTATIONS;
|
||||||
|
|
||||||
|
export function makeABTestingModule(api: ABTestApi): StoreModule<ABTestingState, ABTestingContext> {
|
||||||
|
return {
|
||||||
|
state: new ABTestingState(),
|
||||||
|
mutations: {
|
||||||
|
[SET](state: ABTestingState, values: ABTestValues): void {
|
||||||
|
state.abTestValues = values;
|
||||||
|
},
|
||||||
|
[INIT](state: ABTestingState, isInitialized = true): void {
|
||||||
|
state.abTestingInitialized = isInitialized;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
[FETCH]: async function ({ state, commit }: ABTestingContext): Promise<ABTestValues> {
|
||||||
|
if (!state.abTestingEnabled)
|
||||||
|
return state.abTestValues;
|
||||||
|
const values = await api.fetchABTestValues();
|
||||||
|
|
||||||
|
await commit(SET, values);
|
||||||
|
await commit(INIT);
|
||||||
|
|
||||||
|
return values;
|
||||||
|
},
|
||||||
|
[RESET]: async function ({ commit }: ABTestingContext) {
|
||||||
|
await commit(SET, new ABTestValues());
|
||||||
|
await commit(INIT, false);
|
||||||
|
},
|
||||||
|
[HIT]: async function ({ state, dispatch }: ABTestingContext, action: ABHitAction) {
|
||||||
|
if (!state.abTestingEnabled) return;
|
||||||
|
if (!state.abTestingInitialized) {
|
||||||
|
await dispatch(FETCH);
|
||||||
|
}
|
||||||
|
switch (action) {
|
||||||
|
case ABHitAction.UPGRADE_ACCOUNT_CLICKED:
|
||||||
|
api.sendHit(ABHitAction.UPGRADE_ACCOUNT_CLICKED).catch(_ => {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
35
web/satellite/src/types/abtesting.ts
Normal file
35
web/satellite/src/types/abtesting.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (C) 2022 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes all AB testing related functionality.
|
||||||
|
*/
|
||||||
|
export interface ABTestApi {
|
||||||
|
/**
|
||||||
|
* Used to get all AB testing values.
|
||||||
|
*
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
fetchABTestValues(): Promise<ABTestValues>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to send an action event.
|
||||||
|
*
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
sendHit(action: ABHitAction): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ABTestValues class holds all configurable AB test values.
|
||||||
|
*/
|
||||||
|
export class ABTestValues {
|
||||||
|
public constructor(
|
||||||
|
public hasNewUpgradeBanner: boolean = false,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enum of all AB test hit actions
|
||||||
|
export enum ABHitAction {
|
||||||
|
UPGRADE_ACCOUNT_CLICKED= 'upgrade_clicked'
|
||||||
|
}
|
@ -14,10 +14,17 @@
|
|||||||
class="dashboard__wrap__main-area__content-wrap"
|
class="dashboard__wrap__main-area__content-wrap"
|
||||||
:class="{ 'no-nav': isNavigationHidden }"
|
:class="{ 'no-nav': isNavigationHidden }"
|
||||||
>
|
>
|
||||||
|
<UpgradeNotification
|
||||||
|
v-if="isPaidTierBannerShown && abTestValues.hasNewUpgradeBanner"
|
||||||
|
:open-add-p-m-modal="togglePMModal"
|
||||||
|
/>
|
||||||
<div ref="dashboardContent" class="dashboard__wrap__main-area__content-wrap__container">
|
<div ref="dashboardContent" class="dashboard__wrap__main-area__content-wrap__container">
|
||||||
<div class="bars">
|
<div class="bars">
|
||||||
<BetaSatBar v-if="isBetaSatellite" />
|
<BetaSatBar v-if="isBetaSatellite" />
|
||||||
<PaidTierBar v-if="!creditCards.length && !isOnboardingTour" :open-add-p-m-modal="togglePMModal" />
|
<PaidTierBar
|
||||||
|
v-if="isPaidTierBannerShown && !abTestValues.hasNewUpgradeBanner"
|
||||||
|
:open-add-p-m-modal="togglePMModal"
|
||||||
|
/>
|
||||||
<ProjectInfoBar v-if="isProjectListPage" />
|
<ProjectInfoBar v-if="isProjectListPage" />
|
||||||
<MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" />
|
<MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" />
|
||||||
</div>
|
</div>
|
||||||
@ -80,6 +87,8 @@ import { AuthHttpApi } from '@/api/auth';
|
|||||||
import { MetaUtils } from '@/utils/meta';
|
import { MetaUtils } from '@/utils/meta';
|
||||||
import { AnalyticsHttpApi } from '@/api/analytics';
|
import { AnalyticsHttpApi } from '@/api/analytics';
|
||||||
import eventBus from '@/utils/eventBus';
|
import eventBus from '@/utils/eventBus';
|
||||||
|
import { ABTestValues } from '@/types/abtesting';
|
||||||
|
import { AB_TESTING_ACTIONS } from '@/store/modules/abTesting';
|
||||||
|
|
||||||
import ProjectInfoBar from '@/components/infoBars/ProjectInfoBar.vue';
|
import ProjectInfoBar from '@/components/infoBars/ProjectInfoBar.vue';
|
||||||
import BillingNotification from '@/components/notifications/BillingNotification.vue';
|
import BillingNotification from '@/components/notifications/BillingNotification.vue';
|
||||||
@ -92,6 +101,7 @@ import AllModals from '@/components/modals/AllModals.vue';
|
|||||||
import MobileNavigation from '@/components/navigation/MobileNavigation.vue';
|
import MobileNavigation from '@/components/navigation/MobileNavigation.vue';
|
||||||
import LimitWarningModal from '@/components/modals/LimitWarningModal.vue';
|
import LimitWarningModal from '@/components/modals/LimitWarningModal.vue';
|
||||||
import VBanner from '@/components/common/VBanner.vue';
|
import VBanner from '@/components/common/VBanner.vue';
|
||||||
|
import UpgradeNotification from '@/components/notifications/UpgradeNotification.vue';
|
||||||
|
|
||||||
import LoaderImage from '@/../static/images/common/loader.svg';
|
import LoaderImage from '@/../static/images/common/loader.svg';
|
||||||
|
|
||||||
@ -112,6 +122,7 @@ const {
|
|||||||
BetaSatBar,
|
BetaSatBar,
|
||||||
ProjectInfoBar,
|
ProjectInfoBar,
|
||||||
BillingNotification,
|
BillingNotification,
|
||||||
|
UpgradeNotification,
|
||||||
InactivityModal,
|
InactivityModal,
|
||||||
LimitWarningModal,
|
LimitWarningModal,
|
||||||
VBanner,
|
VBanner,
|
||||||
@ -221,6 +232,7 @@ export default class DashboardArea extends Vue {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch(USER_ACTIONS.GET);
|
await this.$store.dispatch(USER_ACTIONS.GET);
|
||||||
|
await this.$store.dispatch(AB_TESTING_ACTIONS.FETCH);
|
||||||
this.setupSessionTimers();
|
this.setupSessionTimers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$store.subscribeAction((action) => {
|
this.$store.subscribeAction((action) => {
|
||||||
@ -374,6 +386,15 @@ export default class DashboardArea extends Vue {
|
|||||||
LocalData.setSelectedProjectId(projectID);
|
LocalData.setSelectedProjectId(projectID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get abTestValues(): ABTestValues {
|
||||||
|
return this.$store.state.abTestingModule.abTestValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* whether the paid tier banner should be shown */
|
||||||
|
public get isPaidTierBannerShown(): boolean {
|
||||||
|
return !this.$store.state.usersModule.user.paidTier && !this.isOnboardingTour;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if current route is projects list page.
|
* Indicates if current route is projects list page.
|
||||||
*/
|
*/
|
||||||
|
12
web/satellite/static/images/notifications/sunnyicon.svg
Normal file
12
web/satellite/static/images/notifications/sunnyicon.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.35119 6.92862C5.11284 5.9759 5.9759 5.11284 6.92862 5.35119L12.2567 6.68412C12.8864 6.84168 13.5359 6.51073 13.7787 5.90863L15.832 0.814678C16.1992 -0.0961837 17.4047 -0.287119 18.0354 0.465701L21.5624 4.67581C21.9793 5.17344 22.6993 5.28748 23.2495 4.94302L27.9049 2.02887C28.7373 1.50779 29.8248 2.06191 29.8926 3.04165L30.2713 8.52081C30.3161 9.16845 30.8316 9.6839 31.4792 9.72867L36.9584 10.1074C37.9381 10.1752 38.4922 11.2627 37.9711 12.0951L35.057 16.7505C34.7125 17.3007 34.8266 18.0207 35.3242 18.4376L39.5343 21.9646C40.2871 22.5953 40.0962 23.8008 39.1853 24.168L34.0914 26.2213C33.4893 26.4641 33.1583 27.1136 33.3159 27.7433L34.6488 33.0714C34.8872 34.0241 34.0241 34.8872 33.0714 34.6488L27.7433 33.3159C27.1136 33.1583 26.4641 33.4893 26.2213 34.0914L24.168 39.1853C23.8008 40.0962 22.5953 40.2871 21.9646 39.5343L18.4376 35.3242C18.0207 34.8266 17.3007 34.7125 16.7505 35.057L12.0951 37.9711C11.2627 38.4922 10.1752 37.9381 10.1074 36.9584L9.72867 31.4792C9.68391 30.8316 9.16845 30.3161 8.52081 30.2713L3.04165 29.8926C2.06191 29.8248 1.50779 28.7373 2.02888 27.9049L4.94302 23.2495C5.28748 22.6993 5.17344 21.9793 4.67581 21.5624L0.4657 18.0354C-0.287119 17.4047 -0.0961822 16.1992 0.814679 15.832L5.90863 13.7787C6.51073 13.5359 6.84168 12.8864 6.68412 12.2567L5.35119 6.92862Z" fill="#FFEA9F"/>
|
||||||
|
<mask id="mask0_17693_625" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="7" y="7" width="26" height="26">
|
||||||
|
<ellipse cx="19.7987" cy="19.7981" rx="12.6708" ry="12.6708" fill="#FFC700"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_17693_625)">
|
||||||
|
<ellipse cx="19.7987" cy="19.7981" rx="12.6708" ry="12.6708" fill="#FFC700"/>
|
||||||
|
</g>
|
||||||
|
<path d="M3.74219 18.978C3.74219 17.0283 5.3228 15.4478 7.27255 15.4478H32.0779C34.0277 15.4478 35.6083 17.0283 35.6083 18.978V20.3252C35.6083 24.4557 32.2599 27.804 28.1295 27.804C23.9991 27.804 20.6507 24.4557 20.6507 20.3252V20.0001C20.6507 19.4613 20.214 19.0246 19.6752 19.0246C19.1365 19.0246 18.6997 19.4613 18.6997 20.0001V20.3252C18.6997 24.4557 15.3514 27.804 11.221 27.804C7.09056 27.804 3.74219 24.4557 3.74219 20.3252V18.978Z" fill="#FF598B"/>
|
||||||
|
<path d="M5.69336 18.5015C5.69336 17.8925 6.18707 17.3988 6.79609 17.3988H15.6462C16.2552 17.3988 16.7489 17.8925 16.7489 18.5015V20.3253C16.7489 23.3782 14.2741 25.8531 11.2212 25.8531C8.16824 25.8531 5.69336 23.3782 5.69336 20.3253V18.5015Z" fill="black"/>
|
||||||
|
<path d="M22.6016 18.5015C22.6016 17.8925 23.0953 17.3988 23.7043 17.3988H32.5544C33.1634 17.3988 33.6571 17.8925 33.6571 18.5015V20.3253C33.6571 23.3782 31.1823 25.8531 28.1294 25.8531C25.0764 25.8531 22.6016 23.3782 22.6016 20.3253V18.5015Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
18
web/satellite/tests/unit/mock/api/abtesting.ts
Normal file
18
web/satellite/tests/unit/mock/api/abtesting.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (C) 2022 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
import { ABHitAction, ABTestApi, ABTestValues } from '@/types/abtesting';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ABHttpApi is a console AB testing API.
|
||||||
|
* Exposes all ab-testing related functionality
|
||||||
|
*/
|
||||||
|
export class ABMockApi implements ABTestApi {
|
||||||
|
public async fetchABTestValues(): Promise<ABTestValues> {
|
||||||
|
return new ABTestValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendHit(_: ABHitAction): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import { PaymentsMock } from '../mock/api/payments';
|
|||||||
import { ProjectMembersApiMock } from '../mock/api/projectMembers';
|
import { ProjectMembersApiMock } from '../mock/api/projectMembers';
|
||||||
import { ProjectsApiMock } from '../mock/api/projects';
|
import { ProjectsApiMock } from '../mock/api/projects';
|
||||||
import { UsersApiMock } from '../mock/api/users';
|
import { UsersApiMock } from '../mock/api/users';
|
||||||
|
import { ABMockApi } from '../mock/api/abtesting';
|
||||||
|
|
||||||
import { RouteConfig, router } from '@/router';
|
import { RouteConfig, router } from '@/router';
|
||||||
import { makeAccessGrantsModule } from '@/store/modules/accessGrants';
|
import { makeAccessGrantsModule } from '@/store/modules/accessGrants';
|
||||||
@ -24,6 +25,7 @@ import { User } from '@/types/users';
|
|||||||
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
|
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
|
||||||
import { AppState } from '@/utils/constants/appStateEnum';
|
import { AppState } from '@/utils/constants/appStateEnum';
|
||||||
import { NotificatorPlugin } from '@/utils/plugins/notificator';
|
import { NotificatorPlugin } from '@/utils/plugins/notificator';
|
||||||
|
import { makeABTestingModule } from '@/store/modules/abTesting';
|
||||||
import DashboardArea from '@/views/DashboardArea.vue';
|
import DashboardArea from '@/views/DashboardArea.vue';
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
@ -42,6 +44,7 @@ const teamMembersModule = makeProjectMembersModule(new ProjectMembersApiMock());
|
|||||||
const bucketsModule = makeBucketsModule(new BucketsMock());
|
const bucketsModule = makeBucketsModule(new BucketsMock());
|
||||||
const notificationsModule = makeNotificationsModule();
|
const notificationsModule = makeNotificationsModule();
|
||||||
const paymentsModule = makePaymentsModule(new PaymentsMock());
|
const paymentsModule = makePaymentsModule(new PaymentsMock());
|
||||||
|
const abTestingModule = makeABTestingModule(new ABMockApi());
|
||||||
|
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
@ -53,6 +56,7 @@ const store = new Vuex.Store({
|
|||||||
appStateModule,
|
appStateModule,
|
||||||
teamMembersModule,
|
teamMembersModule,
|
||||||
paymentsModule,
|
paymentsModule,
|
||||||
|
abTestingModule,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ exports[`Dashboard renders correctly when data is loaded 1`] = `
|
|||||||
<navigationarea-stub class="dashboard__wrap__main-area__navigation"></navigationarea-stub>
|
<navigationarea-stub class="dashboard__wrap__main-area__navigation"></navigationarea-stub>
|
||||||
<mobilenavigation-stub class="dashboard__wrap__main-area__mobile-navigation"></mobilenavigation-stub>
|
<mobilenavigation-stub class="dashboard__wrap__main-area__mobile-navigation"></mobilenavigation-stub>
|
||||||
<div class="dashboard__wrap__main-area__content-wrap">
|
<div class="dashboard__wrap__main-area__content-wrap">
|
||||||
|
<!---->
|
||||||
<div class="dashboard__wrap__main-area__content-wrap__container">
|
<div class="dashboard__wrap__main-area__content-wrap__container">
|
||||||
<div class="bars">
|
<div class="bars">
|
||||||
<!---->
|
<!---->
|
||||||
|
Loading…
Reference in New Issue
Block a user