Skip to content
Merged
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
48 changes: 44 additions & 4 deletions src/api/forms/service/__stubs__/metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ export function getExpectedOverviewMetrics(timestamp) {
slug: 'form-1-title',
status: 'draft'
},
featureCounts: {},
featureMetrics: {
features: { Sections: 1 },
formStructure: {
conditions: 0,
pages: 1,
questionTypes: 0,
questions: 0,
sections: 1
},
questionTypes: {}
},
submissionsCount: 0,
updatedAt: timestamp
},
Expand All @@ -37,7 +47,17 @@ export function getExpectedOverviewMetrics(timestamp) {
slug: 'form-2-title',
status: 'draft'
},
featureCounts: {},
featureMetrics: {
features: { Sections: 1 },
formStructure: {
conditions: 0,
pages: 1,
questionTypes: 0,
questions: 0,
sections: 1
},
questionTypes: {}
},
submissionsCount: 0,
updatedAt: timestamp
},
Expand All @@ -51,7 +71,17 @@ export function getExpectedOverviewMetrics(timestamp) {
slug: 'form-3-title',
status: 'draft'
},
featureCounts: {},
featureMetrics: {
features: {},
formStructure: {
conditions: 0,
pages: 1,
questionTypes: 0,
questions: 0,
sections: 0
},
questionTypes: {}
},
submissionsCount: 0,
updatedAt: timestamp
}
Expand All @@ -67,7 +97,17 @@ export function getExpectedOverviewMetrics(timestamp) {
slug: 'form-2-title',
status: 'live'
},
featureCounts: {},
featureMetrics: {
features: { Sections: 1 },
formStructure: {
conditions: 0,
pages: 1,
questionTypes: 0,
questions: 0,
sections: 1
},
questionTypes: {}
},
submissionsCount: 0,
updatedAt: timestamp
}
Expand Down
71 changes: 69 additions & 2 deletions src/api/forms/service/report-overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function collectOverviewMetrics(metadata, definition, definitionType) {
formId: metadata.id,
formStatus: metadata.live ? FormStatus.Live : FormStatus.Draft,
summaryMetrics: calcSummaryMetrics(metadata, definition, definitionType),
featureCounts: {},
featureMetrics: calcFeatureMetrics(definition),
submissionsCount: 0,
Comment thread
jbarnsley10 marked this conversation as resolved.
updatedAt: new Date()
}
Expand All @@ -157,6 +157,73 @@ export function calcSummaryMetrics(metadata, definition, definitionType) {
})
}

/**
* @param {FormDefinition} definition
*/
export function calcFeatureMetrics(definition) {
const allComponents = /** @type {ComponentDef[]} */ ([])
for (const page of definition.pages) {
if (hasComponentsEvenIfNoNext(page)) {
allComponents.push(...page.components)
}
}
const questionTypes = getQuestionTypeCounts(allComponents)
return {
questionTypes: Object.fromEntries(questionTypes),
features: getComponentUsageFeatureMetrics(definition),
formStructure: getFormStructureCounts(definition, questionTypes)
}
}

/**
* @param {ComponentDef[]} components
*/
export function getQuestionTypeCounts(components) {
const componentCounts = /** @type {Map<string, number>} */ (new Map())
for (const component of components) {
const count = componentCounts.get(component.type) ?? 0
componentCounts.set(component.type, count + 1)
}
return componentCounts
}

/**
* @param {FormDefinition} definition
*/
export function getComponentUsageFeatureMetrics(definition) {
const features = getFeatureList(definition)
if (definition.pages.some((p) => p.section)) {
features.push('Sections')
}
if (definition.pages.some((p) => p.condition)) {
features.push('Conditional logic')
}
const featureResult = /** @type {Record<string, number>} */ ({})
features.forEach((f) => {
featureResult[f] = 1
})
return featureResult
}

/**
* @param {FormDefinition} definition
* @param {Map<string, number>} questionTypes
*/
export function getFormStructureCounts(definition, questionTypes) {
let numOfQuestions = 0
questionTypes.forEach((value) => {
numOfQuestions += value
})

return {
pages: definition.pages.length,
questions: numOfQuestions,
sections: definition.pages.filter((p) => p.section).length,
conditions: definition.pages.filter((p) => p.condition).length,
questionTypes: questionTypes.size
}
}

/**
* @param {FormDefinition} definition
*/
Expand Down Expand Up @@ -201,5 +268,5 @@ export function getUniqueComponentTypes(definition) {

/**
* @import { ClientSession } from 'mongodb'
* @import { FormDefinition, FormMetadata } from '@defra/forms-model'
* @import { ComponentDef, FormDefinition, FormMetadata } from '@defra/forms-model'
*/
165 changes: 162 additions & 3 deletions src/api/forms/service/report-overview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import * as formDefinition from '~/src/api/forms/repositories/form-definition-re
import { getMetadataCursorOfAllForms } from '~/src/api/forms/repositories/form-metadata-repository.js'
import { getExpectedOverviewMetrics } from '~/src/api/forms/service/__stubs__/metrics.js'
import {
calcFeatureMetrics,
generateReportOverview,
getComponentUsageFeatureMetrics,
getDefinitionIfExists,
getFeatureList,
getQuestionTypeCounts,
getUniqueComponentTypes
} from '~/src/api/forms/service/report-overview.js'
import { client } from '~/src/mongo.js'
Expand Down Expand Up @@ -113,12 +116,22 @@ describe('report-overview', () => {
// @ts-expect-error - resolves to an async iterator like FindCursor<FormMetadataDocument>
.mockReturnValueOnce(mockAsyncIterator)

const pageWithSection = /** @type {FormDefinition} */ ({
pages: [{ section: 'abc' }]
})

// Form 1 - draft and no live
jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({}))
jest
.mocked(formDefinition.get)
.mockResolvedValueOnce(buildDefinition(pageWithSection))

// Form 2 - draft and live
jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({}))
jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({}))
jest
.mocked(formDefinition.get)
.mockResolvedValueOnce(buildDefinition(pageWithSection))
jest
.mocked(formDefinition.get)
.mockResolvedValueOnce(buildDefinition(pageWithSection))

// Form 3 - draft and no live
jest.mocked(formDefinition.get).mockResolvedValueOnce(buildDefinition({}))
Expand Down Expand Up @@ -245,6 +258,148 @@ describe('report-overview', () => {
})
})

describe('calcFeatureMetrics', () => {
it('should return calculated metrics', () => {
const summaryPage = buildSummaryPage({
// @ts-expect-error - forcing the controller type
controller: ControllerType.SummaryWithConfirmationEmail
})
const questionPageId = 'd9c99072-d25d-4688-ab7d-3822cffe802b'
const questionPage = buildQuestionPage({
id: questionPageId,
components: [
buildTextFieldComponent(),
buildTextFieldComponent(),
buildCheckboxComponent(),
buildTextFieldComponent(),
buildCheckboxComponent(),
buildRadioComponent(),
buildDeclarationFieldComponent()
]
})
const fileUploadPage = buildFileUploadPage()
const paymentPage = buildQuestionPage({
components: [buildPaymentComponent()]
})
const sectionPage1 = buildQuestionPage({
section: 'some-section-id1'
})
const sectionPage2 = buildQuestionPage({
section: 'some-section-id2'
})

const definition = buildDefinition({
pages: [
questionPage,
fileUploadPage,
sectionPage1,
sectionPage2,
paymentPage,
summaryPage
]
})
expect(calcFeatureMetrics(definition)).toEqual({
features: {
'File upload': 1,
'Email confirmation': 1,
'GOV.UK Pay': 1,
Declarations: 1,
Sections: 1
},
formStructure: {
conditions: 0,
pages: 6,
questionTypes: 6,
questions: 9,
sections: 2
},
questionTypes: {
CheckboxesField: 2,
DeclarationField: 1,
FileUploadField: 1,
PaymentField: 1,
RadiosField: 1,
TextField: 3
}
})
})
})

describe('getQuestionTypeCounts', () => {
it('should return counts', () => {
const components = [
buildTextFieldComponent(),
buildTextFieldComponent(),
buildCheckboxComponent(),
buildTextFieldComponent(),
buildRadioComponent(),
buildCheckboxComponent(),
buildTextFieldComponent(),
buildRadioComponent(),
buildCheckboxComponent(),
buildTextFieldComponent(),
buildDeclarationFieldComponent()
]
expect(Object.fromEntries(getQuestionTypeCounts(components))).toEqual({
CheckboxesField: 3,
DeclarationField: 1,
RadiosField: 2,
TextField: 5
})
})
})

describe('get component usage features', () => {
it('should return list of features', () => {
const summaryPage = buildSummaryPage({
// @ts-expect-error - forcing the controller type
controller: ControllerType.SummaryWithConfirmationEmail
})
const questionPageId = 'd9c99072-d25d-4688-ab7d-3822cffe802b'
const questionPage = buildQuestionPage({
id: questionPageId,
components: [
buildTextFieldComponent(),
buildTextFieldComponent(),
buildCheckboxComponent(),
buildTextFieldComponent(),
buildCheckboxComponent(),
buildRadioComponent(),
buildDeclarationFieldComponent()
]
})
const fileUploadPage = buildFileUploadPage()
const paymentPage = buildQuestionPage({
components: [buildPaymentComponent()]
})
const conditionPage = buildQuestionPage({
condition: 'some-condition-id'
})
const sectionPage = buildQuestionPage({
section: 'some-section-id'
})

const definition = buildDefinition({
pages: [
questionPage,
fileUploadPage,
paymentPage,
conditionPage,
sectionPage,
summaryPage
]
})
expect(getComponentUsageFeatureMetrics(definition)).toEqual({
'File upload': 1,
'Email confirmation': 1,
'GOV.UK Pay': 1,
Declarations: 1,
Sections: 1,
'Conditional logic': 1
})
})
})

describe('getDefinitionIfExists', () => {
it('should not throw if error is NOT_FOUND', async () => {
jest.mocked(formDefinition.get).mockImplementationOnce(() => {
Expand All @@ -268,3 +423,7 @@ describe('report-overview', () => {
})
})
})

/**
* @import { FormDefinition } from '@defra/forms-model'
*/
Loading