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

Refine usage report #1323

Open
wants to merge 6 commits 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
54 changes: 36 additions & 18 deletions scripts/analytics/usage-report/WorkbookGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import EventCode from "@moj-bichard7/common/types/EventCode"
import { ReportData, ReportDataResult, reportEventTitles } from "./common"

const excel = require("excel4node")
Expand All @@ -20,11 +19,12 @@ export default class WorkbookGenerator {
}

private getEventTitle = (eventCode: string) => {
const eventTitlePrefix = eventCode.split(".")[0] === "new-ui" ? "New UI" : "Old UI"
const eventTitlePrefix =
eventCode.split(".")[0] === "new-ui" ? "New: " : eventCode.split(".")[0] === "old-ui" ? "Old: " : ""
const actualEventCode = eventCode.split(".").slice(1).join(".")
const eventTitle = reportEventTitles[actualEventCode] ?? actualEventCode

return `${eventTitlePrefix} ${eventTitle}`
return `${eventTitlePrefix}${eventTitle}`
}

public generate = () => {
Expand All @@ -35,12 +35,8 @@ export default class WorkbookGenerator {
this.generateUserReport(this.reportDataResult.allEvents, "All")
this.generateDailyUserReport(this.reportDataResult.allEvents, "All")
this.generateMonthlyUserReport(this.reportDataResult.allEvents, "All")
this.generateMonthlyForceReport(this.reportDataResult.allEvents, "All")

this.generateDailyReport(this.reportDataResult.withNewUiEvents, "New UI")
this.generateMonthlyReport(this.reportDataResult.withNewUiEvents, "New UI")
this.generateUserReport(this.reportDataResult.withNewUiEvents, "New UI")
this.generateDailyUserReport(this.reportDataResult.withNewUiEvents, "New UI")
this.generateMonthlyUserReport(this.reportDataResult.withNewUiEvents, "New UI")
return this
}

Expand Down Expand Up @@ -137,6 +133,26 @@ export default class WorkbookGenerator {
this.addFooterRow(sheet, reportData, rowIndex - 2, true)
}

private generateMonthlyForceReport = (reportData: ReportData, title: string) => {
const sheet = this.workbook.addWorksheet(`Forces Monthly (${title})`)
this.addHeaderRow(sheet, reportData, "Date", "Force")

let rowIndex = 2
Object.entries(reportData.monthlyForces).forEach(([date, forceData]) => {
Object.entries(forceData).forEach(([force, data]) => {
sheet.cell(rowIndex, 1).string(date).style(this.mainCellStyle)
sheet.cell(rowIndex, 2).string(force).style(this.mainCellStyle)
reportData.eventCodes.forEach((eventCode, columnIndex) => {
sheet.cell(rowIndex, columnIndex + 3).number(data[eventCode] || 0)
})

rowIndex += 1
})
})

this.addFooterRow(sheet, reportData, rowIndex - 2, true)
}

private addHeaderRow = (sheet, reportData: ReportData, firstColumnName: string, secondColumnName?: string) => {
sheet.cell(1, 1).string(firstColumnName).style(this.mainCellStyle)
if (secondColumnName) {
Expand All @@ -145,8 +161,10 @@ export default class WorkbookGenerator {

const startColumnIndex = 2 + (secondColumnName ? 1 : 0)
reportData.eventCodes.forEach((eventCode, index) => {
const eventTitle =
(eventCode.split(".")[0] === "new-ui" ? "New UI " : "Old UI ") + eventCode.split(".").slice(1).join()
// REVIEW AND REMOVE
// const eventTitle =
// (eventCode.split(".")[0] === "new-ui" ? "New UI " : eventCode.split(".")[0] === "old-ui" ? "Old UI " : "") +
// eventCode.split(".").slice(1).join()
sheet
.cell(1, startColumnIndex + index)
.string(this.getEventTitle(eventCode))
Expand Down Expand Up @@ -174,27 +192,27 @@ export default class WorkbookGenerator {
// XX People have the new UI turned on
sheet.cell(rowIndex++, 1).string(`${this.usersWithAccessToNewUi.length} People have the new UI turned on`)

Object.entries(reportDataResult.withNewUiEvents.monthly).forEach(([date, newUiMonthlyData]) => {
Object.entries(reportDataResult.allEvents.monthly).forEach(([date, monthlyData]) => {
sheet.cell(rowIndex++, 1).string(date).style(this.mainCellStyle)
const newUiMonthlyUsersData = reportDataResult.withNewUiEvents.monthlyUsers[date]
const monthlyUsersData = reportDataResult.allEvents.monthlyUsers[date]

// XX People resolved a trigger on the new UI
const totalUsersResolvedTriggersInNewUi = Object.entries(newUiMonthlyUsersData).filter(
const totalUsersResolvedTriggersInNewUi = Object.entries(monthlyUsersData).filter(
([_, monthlyUserData]) => (monthlyUserData["new-ui.triggers.resolved"] || 0) > 0
).length
sheet.cell(rowIndex++, 1).string(`${totalUsersResolvedTriggersInNewUi} People resolved a trigger on the new UI`)

// XX People resolved an exception on the new UI
const totalUsersResolvedExceptionsInNewUi = Object.entries(newUiMonthlyUsersData).filter(
const totalUsersResolvedExceptionsInNewUi = Object.entries(monthlyUsersData).filter(
([_, monthlyUserData]) => (monthlyUserData["new-ui.exceptions.resolved"] || 0) > 0
).length
sheet
.cell(rowIndex++, 1)
.string(`${totalUsersResolvedExceptionsInNewUi} People resolved an exception on the new UI`)

// Of XX people who have the UI turned on, XX.XX% of their triggers were resolved in the new UI
const oldUiTriggersResolved = newUiMonthlyData["old-ui.triggers.resolved"] || 0
const newUiTriggersResolved = newUiMonthlyData["new-ui.triggers.resolved"] || 0
const oldUiTriggersResolved = monthlyData["old-ui.triggers.resolved"] || 0
const newUiTriggersResolved = monthlyData["new-ui.triggers.resolved"] || 0
const totalTriggersResolved = oldUiTriggersResolved + newUiTriggersResolved
// prettier-ignore
const triggersResolvedInNewUiPercentage = (totalTriggersResolved ? newUiTriggersResolved / totalTriggersResolved * 100 : 0 ).toFixed(2)
Expand All @@ -205,8 +223,8 @@ export default class WorkbookGenerator {
)

// Of XX people who have the UI turned on, XX.XX% of their exceptions were resolved in the new UI
const oldUiExceptionResolved = newUiMonthlyData["old-ui.exceptions.resolved"] || 0
const newUiExceptionResolved = newUiMonthlyData["new-ui.exceptions.resolved"] || 0
const oldUiExceptionResolved = monthlyData["old-ui.exceptions.resolved"] || 0
const newUiExceptionResolved = monthlyData["new-ui.exceptions.resolved"] || 0
const totalExceptionsResolved = oldUiExceptionResolved + newUiExceptionResolved
// prettier-ignore
const exceptionsResolvedInNewUiPercentage = (totalExceptionsResolved ? newUiExceptionResolved / totalExceptionsResolved * 100 : 0).toFixed(2)
Expand Down
54 changes: 45 additions & 9 deletions scripts/analytics/usage-report/common.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,55 @@
import { AuditLogEvent } from "../../../packages/common/types/AuditLogEvent"
import EventCode from "@moj-bichard7/common/types/EventCode"
import { AuditLogEvent } from "../../../packages/common/types/AuditLogEvent"

type FullAuditLogEvent = AuditLogEvent & { _messageId: string }

type ReportData = {
summary: Record<EventCode, number>
summary: { [key in EventCode]?: number }
users: Record<string, Record<EventCode, number>>
daily: Record<string, Record<EventCode, number>>
monthly: Record<string, Record<EventCode, number>>
dailyUsers: Record<string, Record<string, Record<EventCode, number>>>
monthlyUsers: Record<string, Record<string, Record<EventCode, number>>>
monthlyForces: Record<string, Record<string, Record<EventCode, number>>>
eventCodes: string[]
}

type ReportDataResult = {
allEvents: ReportData
withNewUiEvents: ReportData,
usersWithNewUiEvent: string[]
}

const reportEventCodes: EventCode[] = [
EventCode.AllTriggersResolved,
EventCode.TriggersLocked,
EventCode.TriggersUnlocked,
EventCode.TriggersResolved,
EventCode.TriggersGenerated,
EventCode.ExceptionsLocked,
EventCode.ExceptionsUnlocked,
EventCode.ExceptionsResolved,
EventCode.HearingOutcomeReallocated
EventCode.ExceptionsGenerated,
EventCode.HearingOutcomeReallocated,
EventCode.HearingOutcomeResubmittedPhase1,
EventCode.HearingOutcomeResubmittedPhase2,
EventCode.HearingOutcomeDetails,
EventCode.PncResponseNotReceived,
EventCode.PncResponseReceived,
EventCode.IgnoredAlreadyOnPNC,
EventCode.IgnoredAncillary,
EventCode.IgnoredAppeal,
EventCode.IgnoredDisabled,
EventCode.IgnoredNoOffences,
EventCode.IgnoredNonrecordable,
EventCode.IgnoredReopened
]

const eventCodesToDisplay: (EventCode | string)[] = [
"Total triggers",
EventCode.TriggersResolved,
"Total exceptions",
EventCode.ExceptionsResolved,
"Resolved exceptions",
"Exceptions resubmitted"
]

const reportEventTitles = {
Expand All @@ -35,9 +59,11 @@ const reportEventTitles = {
[EventCode.TriggersResolved]: "Triggers resolved",
[EventCode.ExceptionsLocked]: "Exception locked",
[EventCode.ExceptionsUnlocked]: "Exception unlocked",
[EventCode.ExceptionsResolved]: "Exception resolved",
[EventCode.HearingOutcomeReallocated]: "Case reallocated"
} as Record<EventCode, string>
[EventCode.ExceptionsResolved]: "Exception manually resolved",
[EventCode.HearingOutcomeReallocated]: "Case reallocated",
[EventCode.HearingOutcomeResubmittedPhase1]: "Resubmitted to Phase 1",
[EventCode.HearingOutcomeResubmittedPhase2]: "Resubmitted to Phase 2"
} as Record<EventCode | string, string>

const log = (...params: unknown[]) => {
const logContent = [new Date().toISOString(), " - ", ...params]
Expand All @@ -48,4 +74,14 @@ const getDateString = (date: string | Date) => (typeof date === "object" ? date.

const isNewUIEvent = (event: AuditLogEvent) => event.eventSource === "Bichard New UI"

export { reportEventCodes, reportEventTitles, ReportData, ReportDataResult, getDateString, log, isNewUIEvent }
export {
eventCodesToDisplay,
FullAuditLogEvent,
getDateString,
isNewUIEvent,
log,
ReportData,
ReportDataResult,
reportEventCodes,
reportEventTitles
}
78 changes: 67 additions & 11 deletions scripts/analytics/usage-report/fetchEvents.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { AuditLogEvent } from "../../../packages/common/types/AuditLogEvent"
import { DocumentClient } from "aws-sdk/clients/dynamodb"
import { isError } from "@moj-bichard7/common/types/Result"
import { reportEventCodes, getDateString, isNewUIEvent, log } from "./common"
import { DocumentClient } from "aws-sdk/clients/dynamodb"
import { FullAuditLogEvent, getDateString, isNewUIEvent, log, reportEventCodes } from "./common"

const generateDates = (start: Date, end: Date): Date[] => {
const dates: Date[] = []
let currentDate = new Date(start)
while (currentDate < end) {
dates.push(new Date(currentDate))
currentDate.setDate(currentDate.getDate() + 1)
}

return dates
}

const filterEvents = (events: AuditLogEvent[]): AuditLogEvent[] =>
const filterEvents = (events: FullAuditLogEvent[]): FullAuditLogEvent[] =>
events.filter((event) => reportEventCodes.includes(event.eventCode))

const findEvents = async (dynamo: DocumentClient, eventsTableName: string, start: Date, end: Date) => {
log(`Getting messages for the period between ${getDateString(start)} and ${getDateString(end)}`)
const fetchEvents = async (dynamo: DocumentClient, eventsTableName: string, start: Date, end: Date) => {
let lastEvaluatedKey
let events: AuditLogEvent[] = []
const messageIds = new Set<string>()
console.log("Fetching messages and events...")
let events: FullAuditLogEvent[] = []

while (true) {
const query: DocumentClient.QueryInput = {
Expand Down Expand Up @@ -40,13 +48,13 @@ const findEvents = async (dynamo: DocumentClient, eventsTableName: string, start
return eventsResult
}

if (eventsResult.Items.length === 0) {
if (!eventsResult.Items || eventsResult.Items.length === 0) {
return events
}

lastEvaluatedKey = eventsResult?.LastEvaluatedKey
let fetchedEvents = (eventsResult.Items ?? []) as (AuditLogEvent & { _messageId: string })[]
fetchedEvents = filterEvents(fetchedEvents) as (AuditLogEvent & { _messageId: string })[]
let fetchedEvents = (eventsResult.Items ?? []) as FullAuditLogEvent[]
fetchedEvents = filterEvents(fetchedEvents) as FullAuditLogEvent[]

fetchedEvents.forEach((event) => messageIds.add(event._messageId))
events = events.concat(filterEvents(fetchedEvents))
Expand All @@ -69,4 +77,52 @@ const findEvents = async (dynamo: DocumentClient, eventsTableName: string, start
}
}

const findEvents = async (
dynamo: DocumentClient,
eventsTableName: string,
start: Date,
end: Date
): Promise<FullAuditLogEvent[] | Error> => {
log(`Getting messages for the period between ${getDateString(start)} and ${getDateString(end)}`)
const allEvents: FullAuditLogEvent[] = []
console.log(`Fetching messages and events between ${start.toISOString()} and ${end.toISOString()}...`)
const dates = generateDates(start, end)
const totalDates = dates.length

const worker = async () => {
while (dates.length > 0) {
const date = dates.shift()
if (!date) {
break
}

const endDate = new Date(date)
endDate.setDate(endDate.getDate() + 1)
const events = await fetchEvents(dynamo, eventsTableName, date, endDate)
if (isError(events)) {
throw events
}

allEvents.push(...events)
}
}

const reporter = async () => {
while (dates.length > 0) {
console.log(`Fetch events for dates ${totalDates - dates.length} of ${totalDates}`)

await new Promise((resolve) => setTimeout(resolve, 3000))
}
}

await Promise.all(
new Array(10)
.fill(0)
.map(() => worker())
.concat(reporter())
)

return allEvents.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1))
}

export default findEvents
69 changes: 69 additions & 0 deletions scripts/analytics/usage-report/fetchForceOwners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import EventCode from "@moj-bichard7/common/types/EventCode"
import { isError } from "@moj-bichard7/common/types/Result"
import { DocumentClient } from "aws-sdk/clients/dynamodb"
import { FullAuditLogEvent } from "./common"
import getForceOwner from "./getForceOwner"

const fetchForceOwners = async (
events: FullAuditLogEvent[],
dynamo: DocumentClient,
auditLogTableName: string
): Promise<Record<string, number>> => {
const messageIdsSet = events.reduce((acc, event) => {
acc.add(event._messageId)
return acc
}, new Set<string>())

const messageIds = Array.from(messageIdsSet)
const forceOwners: Record<string, number> = {}
events
.filter((event) => event.eventCode === EventCode.HearingOutcomeDetails)
.forEach((event) => {
const forceOwner = event.attributes?.["Force Owner"]?.toString().substring(0, 2) as string
if (forceOwner) {
forceOwners[event._messageId] = Number(forceOwner)
}
})
const totalMessages = messageIds.length

const worker = async () => {
while (messageIds.length > 0) {
const messageId = messageIds.shift()
if (!messageId) {
break
}

if (forceOwners[messageId]) {
continue
}

const forceOwner = await getForceOwner(dynamo, auditLogTableName, messageId)
if (isError(forceOwner)) {
throw forceOwner
}

forceOwners[messageId] = Number(forceOwner)
}
}

const reporter = async () => {
while (messageIds.length > 0) {
console.log(`Fetched force owner for ${totalMessages - messageIds.length} of ${totalMessages}`)

await new Promise((resolve) => setTimeout(resolve, 3000))
}
}

await Promise.all(
new Array(50)
.fill(0)
.map(() => worker())
.concat(reporter())
)

console.log(`Fetched force owner for ${totalMessages - messageIds.length} of ${totalMessages}`)

return forceOwners
}

export default fetchForceOwners
Loading
Loading