Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Move feature analytics to project level #5100

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion frontend/common/dispatcher/action-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const Actions = Object.assign({}, require('./base/_action-constants'), {
'GET_CHANGE_REQUEST': 'GET_CHANGE_REQUEST',
'GET_CHANGE_REQUESTS': 'GET_CHANGE_REQUESTS',
'GET_ENVIRONMENT': 'GET_ENVIRONMENT',
'GET_FEATURE_USAGE': 'GET_FEATURE_USAGE',
'GET_FLAGS': 'GET_FLAGS',
'GET_IDENTITY': 'GET_IDENTITY',
'GET_IDENTITY_SEGMENTS': 'GET_IDENTITY_SEGMENTS',
Expand Down
9 changes: 0 additions & 9 deletions frontend/common/dispatcher/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,15 +222,6 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), {
page,
})
},
getFeatureUsage(projectId, environmentId, flag, period) {
Dispatcher.handleViewAction({
actionType: Actions.GET_FEATURE_USAGE,
environmentId,
flag,
period,
projectId,
})
},
getFeatures(
projectId,
environmentId,
Expand Down
2 changes: 0 additions & 2 deletions frontend/common/providers/FeatureListProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const FeatureListProvider = class extends React.Component {
maxFeaturesAllowed: ProjectStore.getMaxFeaturesAllowed(),
projectFlags: FeatureListStore.getProjectFlags(),
totalFeatures: ProjectStore.getTotalFeatures(),
usageData: FeatureListStore.getFeatureUsage(),
})
})
this.listenTo(FeatureListStore, 'removed', (data) => {
Expand All @@ -44,7 +43,6 @@ const FeatureListProvider = class extends React.Component {
isLoading: FeatureListStore.isLoading,
isSaving: FeatureListStore.isSaving,
lastSaved: FeatureListStore.getLastSaved(),
usageData: FeatureListStore.getFeatureUsage(),
})
this.props.onError && this.props.onError(FeatureListStore.error)
})
Expand Down
90 changes: 90 additions & 0 deletions frontend/common/services/useFeatureAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'
import { sortBy } from 'lodash'
import moment from 'moment'
import range from 'lodash/range'

export const featureAnalyticService = service
.enhanceEndpoints({ addTagTypes: ['FeatureAnalytic'] })
.injectEndpoints({
endpoints: (builder) => ({
getFeatureAnalytics: builder.query<
Res['featureAnalytics'],
Req['getFeatureAnalytics']
>({
providesTags: [{ id: 'LIST', type: 'FeatureAnalytic' }],
queryFn: async (query, baseQueryApi, extraOptions, baseQuery) => {
const responses = await Promise.all(
query.environment_ids.map((environment_id) => {
return baseQuery({
url: `projects/${query.project_id}/features/${query.feature_id}/evaluation-data/?period=${query.period}&environment_id=${environment_id}`,
})
}),
)

const error = responses.find((v) => !!v.error)?.error
const today = moment().startOf('day')
const startDate = moment(today).subtract(query.period - 1, 'days')
const preBuiltData: Res['featureAnalytics'] = []
for (
let date = startDate.clone();
date.isSameOrBefore(today);
date.add(1, 'days')
) {
const dayObj: Res['featureAnalytics'][number] = {
day: date.format('Do MMM'),
}
query.environment_ids.forEach((envId) => {
dayObj[envId] = 0
})
preBuiltData.push(dayObj)
}

responses.forEach((response, i) => {
const environment_id = query.environment_ids[i]

response.data.forEach((entry: Res['featureAnalytics'][number]) => {
const date = moment(entry.day).format('Do MMM')
const dayEntry = preBuiltData.find((d) => d.day === date)
if (dayEntry) {
dayEntry[environment_id] = entry.count // Set count for specific environment ID
}
})
})
return {
data: error ? [] : preBuiltData,
error,
}
},
}),
// END OF ENDPOINTS
}),
})

export async function getFeatureAnalytics(
store: any,
data: Req['getFeatureAnalytics'],
options?: Parameters<
typeof featureAnalyticService.endpoints.getFeatureAnalytics.initiate
>[1],
) {
return store.dispatch(
featureAnalyticService.endpoints.getFeatureAnalytics.initiate(
data,
options,
),
)
}
// END OF FUNCTION_EXPORTS

export const {
useGetFeatureAnalyticsQuery,
// END OF EXPORTS
} = featureAnalyticService

/* Usage examples:
const { data, isLoading } = useGetFeatureAnalyticsQuery({ id: 2 }, {}) //get hook
const [createFeatureAnalytics, { isLoading, data, isSuccess }] = useCreateFeatureAnalyticsMutation() //create hook
featureAnalyticService.endpoints.getFeatureAnalytics.select({id: 2})(store.getState()) //access data from any function
*/
2 changes: 1 addition & 1 deletion frontend/common/stores/account-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import dataRelay from 'data-relay'
import { sortBy } from 'lodash'
import Project from 'common/project'
import { getStore } from 'common/store'
import { service } from "common/service";
import { service } from 'common/service'

const controller = {
acceptInvite: (id) => {
Expand Down
81 changes: 18 additions & 63 deletions frontend/common/stores/feature-list-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import {
} from 'common/services/useProjectFlag'
import OrganisationStore from './organisation-store'
import {
ChangeRequest,
Environment,
FeatureState,
PagedResponse,
ProjectFlag, TypedFeatureState,
} from 'common/types/responses';
ChangeRequest,
Environment,
FeatureState,
PagedResponse,
ProjectFlag,
TypedFeatureState,
} from 'common/types/responses'
import Utils from 'common/utils/utils'
import Actions from 'common/dispatcher/action-constants'
import Project from 'common/project'
Expand Down Expand Up @@ -472,16 +473,17 @@ const controller = {
API.trackEvent(Constants.events.EDIT_FEATURE)
const env: Environment = ProjectStore.getEnvironment(environmentId) as any
// Detect differences between change request and existing feature states
const res: { data: PagedResponse<TypedFeatureState> } = await getFeatureStates(
getStore(),
{
environment: environmentFlag.environment,
feature: projectFlag.id,
},
{
forceRefetch: true,
},
)
const res: { data: PagedResponse<TypedFeatureState> } =
await getFeatureStates(
getStore(),
{
environment: environmentFlag.environment,
feature: projectFlag.id,
},
{
forceRefetch: true,
},
)
const segmentResult = await getSegments(getStore(), {
include_feature_specific: true,
page_size: 1000,
Expand Down Expand Up @@ -800,42 +802,6 @@ const controller = {
API.ajaxHandler(store, e)
})
},
getFeatureUsage(projectId, environmentId, flag, period) {
data
.get(
`${Project.api}projects/${projectId}/features/${flag}/evaluation-data/?period=${period}&environment_id=${environmentId}`,
)
.then((result) => {
const firstResult = result[0]
const lastResult = firstResult && result[result.length - 1]
const diff = firstResult
? moment(lastResult.day, 'YYYY-MM-DD').diff(
moment(firstResult.day, 'YYYY-MM-DD'),
'days',
)
: 0
if (firstResult && diff) {
_.range(0, diff).map((v) => {
const day = moment(firstResult.day)
.add(v, 'days')
.format('YYYY-MM-DD')
if (!result.find((v) => v.day === day)) {
result.push({
'count': 0,
day,
})
}
})
}
store.model.usageData = _.sortBy(result, (v) =>
moment(v.day, 'YYYY-MM-DD').valueOf(),
).map((v) => ({
...v,
day: moment(v.day, 'YYYY-MM-DD').format('Do MMM'),
}))
store.changed()
})
},
getFeatures: (projectId, environmentId, force, page, filter, pageSize) => {
if (!store.model || store.envId !== environmentId || force) {
store.envId = environmentId
Expand Down Expand Up @@ -975,9 +941,6 @@ const store = Object.assign({}, BaseStore, {
getEnvironmentFlags() {
return store.model && store.model.keyedEnvironmentFeatures
},
getFeatureUsage() {
return store.model && store.model.usageData
},
getLastSaved() {
return store.model && store.model.lastSaved
},
Expand Down Expand Up @@ -1041,14 +1004,6 @@ store.dispatcherIndex = Dispatcher.register(store, (payload) => {
)
}
break
case Actions.GET_FEATURE_USAGE:
controller.getFeatureUsage(
action.projectId,
action.environmentId,
action.flag,
action.period,
)
break
case Actions.CREATE_FLAG:
controller.createFlag(action.projectId, action.environmentId, action.flag)
break
Expand Down
7 changes: 7 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export type Req = {
environmentId?: string
tags?: string[]
is_archived?: boolean
search?: string
}
getProjectFlag: { project: string | number; id: string }
getRolesPermissionUsers: { organisation_id: number; role_id: number }
Expand Down Expand Up @@ -540,5 +541,11 @@ export type Req = {
environmentId: string
data: Identity
}
getFeatureAnalytics: {
project_id: string
feature_id: string
period: number
environment_ids: string[]
}
// END OF TYPES
}
4 changes: 4 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,5 +784,9 @@ export type Res = {
metadata_xml: string
}
samlAttributeMapping: PagedResponse<SAMLAttributeMapping>
featureAnalytics: {
day: string
[environmentId: string]: string | number | undefined // Dynamic properties for environment IDs
}[]
// END OF TYPES
}
16 changes: 12 additions & 4 deletions frontend/web/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import HomeAside from './pages/HomeAside'
import ScrollToTop from './ScrollToTop'
import AnnouncementPerPage from './AnnouncementPerPage'
import Announcement from './Announcement'
import AccountProvider from 'common/providers/AccountProvider'

const App = class extends Component {
static propTypes = {
Expand Down Expand Up @@ -216,9 +217,7 @@ const App = class extends Component {
this.context.router.history.replace(redirect)
} else {
AsyncStorage.getItem('lastEnv').then((res) => {
if (
this.props.location.search.includes('github-redirect')
) {
if (this.props.location.search.includes('github-redirect')) {
this.context.router.history.replace(
`/github-setup${this.props.location.search}`,
)
Expand Down Expand Up @@ -594,6 +593,15 @@ const App = class extends Component {
>
Compare
</NavSubLink>
{!Project.disableAnalytics && (
<NavSubLink
icon={<Icon name='bar-chart' fill='#9DA4AE' />}
id='feature-analytics-link'
to={`/project/${projectId}/feature-analytics`}
>
Feature Analytics
</NavSubLink>
)}
<Permission
level='project'
permission='ADMIN'
Expand Down Expand Up @@ -662,7 +670,7 @@ const App = class extends Component {
<NavSubLink
icon={<SettingsIcon />}
id='org-settings-link'
data-test='org-settings-link'
data-test='org-settings-link'
to={`/organisation/${
AccountStore.getOrganisation().id
}/settings`}
Expand Down
4 changes: 2 additions & 2 deletions frontend/web/components/CompareFeatures.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// import propTypes from 'prop-types';
import React, { Component } from 'react'
import FlagSelect from './FlagSelect'
import FeatureSelect from './FeatureSelect'
import ProjectStore from 'common/stores/project-store'
import data from 'common/data/base/_data'
import FeatureRow from './FeatureRow'
Expand Down Expand Up @@ -78,7 +78,7 @@ class CompareEnvironments extends Component {
<Row>
<Row>
<div style={{ width: featureNameWidth }}>
<FlagSelect
<FeatureSelect
placeholder='Select a Feature...'
projectId={this.props.projectId}
onChange={(flagId, flag) =>
Expand Down
Loading
Loading