Skip to content

Commit

Permalink
ASAP-376 Process Productivity Metrics (#4286)
Browse files Browse the repository at this point in the history
* ASAP-376 Process Productivity Metrics
  • Loading branch information
gabiayako authored May 27, 2024
1 parent 5431ce1 commit e4d5d2b
Show file tree
Hide file tree
Showing 8 changed files with 619 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/reusable-crn-analytics-algolia-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,11 @@ jobs:
ALGOLIA_APP_ID: ${{ steps.algolia-keys.outputs.algolia-app-id }}
ALGOLIA_INDEX: ${{ steps.setup.outputs.crn-analytics-algolia-index }}
ALGOLIA_INDEX_TEMP: '${{ steps.setup.outputs.crn-analytics-algolia-index }}_${{ github.run_id }}_temp'
- name: Process Productivity Performance
if: ${{ inputs.metric == 'all' || inputs.metric == 'team-productivity' || inputs.metric == 'user-productivity' }}
run: yarn algolia:process-productivity-performance -a $ALGOLIA_APP_ID -k $ALGOLIA_API_KEY -n $ALGOLIA_INDEX -m $METRIC
env:
ALGOLIA_API_KEY: ${{ steps.algolia-keys.outputs.algolia-api-key }}
ALGOLIA_APP_ID: ${{ steps.algolia-keys.outputs.algolia-app-id }}
ALGOLIA_INDEX: ${{ steps.setup.outputs.crn-analytics-algolia-index }}
METRIC: ${{ inputs.metric }}
1 change: 1 addition & 0 deletions apps/asap-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"algolia:clear-index": "ts-node --transpile-only src/cli.ts algolia:clear-index",
"algolia:get-settings": "ts-node --transpile-only src/cli.ts algolia:get-settings",
"algolia:move-index": "ts-node --transpile-only src/cli.ts algolia:move-index",
"algolia:process-productivity-performance": "ts-node --transpile-only src/cli.ts algolia:process-productivity-performance",
"algolia:remove-records": "ts-node --transpile-only src/cli.ts algolia:remove-records",
"algolia:set-settings": "ts-node --transpile-only src/cli.ts algolia:set-settings",
"algolia:set-analytics-settings": "ts-node --transpile-only src/cli.ts algolia:set-analytics-settings",
Expand Down
36 changes: 36 additions & 0 deletions apps/asap-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
removeAlgoliaRecords,
setAlgoliaAnalyticsSettings,
setAlgoliaSettings,
processProductivityPerformance,
} from './scripts/algolia';

const stringType = 'string' as const;
Expand Down Expand Up @@ -49,11 +50,29 @@ const appNameOption = {
demandOption: trueType,
};

enum ProductivityMetricOption {
all = 'all',
'team-productivity' = 'team-productivity',
'user-productivity' = 'user-productivity',
}

const productivityMetricOption = {
alias: 'm',
description: 'Productivity Metric',
choices: Object.values(ProductivityMetricOption),
default: ProductivityMetricOption.all,
};

type BaseArguments = {
appid: string;
apikey: string;
};

interface ProcessProductivityPerformanceArguments extends BaseArguments {
index: string;
metric: 'all' | 'user-productivity' | 'team-productivity';
}

interface DeleteIndexArguments extends BaseArguments {
index: string;
}
Expand Down Expand Up @@ -87,6 +106,23 @@ interface SetAnalyticsSettings extends BaseArguments {

// eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-floating-promises
yargs(hideBin(process.argv))
.command<ProcessProductivityPerformanceArguments>({
command: 'algolia:process-productivity-performance',
describe: 'process productivity performance',
builder: (cli) =>
cli
.option('appid', appIdOption)
.option('apikey', apikeyOption)
.option('index', indexOption)
.option('metric', productivityMetricOption),
handler: async ({ index, appid, apikey, metric }) =>
processProductivityPerformance({
algoliaAppId: appid,
algoliaCiApiKey: apikey,
indexName: index,
metric,
}),
})
.command<DeleteIndexArguments>({
command: 'algolia:delete-index',
describe: 'deletes the index',
Expand Down
1 change: 1 addition & 0 deletions apps/asap-cli/src/scripts/algolia/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './clear-index';
export * from './delete-index';
export * from './get-settings';
export * from './move-index';
export * from './process-productivity-performance';
export * from './remove-records';
export * from './set-settings';
export * from './set-analytics-settings';
249 changes: 249 additions & 0 deletions apps/asap-cli/src/scripts/algolia/process-productivity-performance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { SearchResponse } from '@algolia/client-search';
import {
PerformanceMetrics,
TeamOutputDocumentType,
teamOutputDocumentTypes,
timeRanges,
} from '@asap-hub/model';
import type { SearchIndex } from 'algoliasearch';
import algoliasearch from 'algoliasearch';

type RoundingType = 'ceil' | 'floor';

const roundToTwoDecimals = (number: number, type?: RoundingType): number => {
const factor = 100;

if (!type) {
return parseFloat(number.toFixed(2));
}

return type === 'ceil'
? Math.ceil(number * factor) / factor
: Math.floor(number * factor) / factor;
};

export const getStandardDeviation = (
numbers: number[],
mean: number,
): number => {
const n = numbers.length;
if (n === 0) return 0;

const variance =
numbers.reduce((sum, value) => sum + (value - mean) ** 2, 0) / n;

return Math.sqrt(variance);
};

export const getBellCurveMetrics = (
data: number[],
isInteger: boolean = true,
): PerformanceMetrics => {
const mean = data.reduce((sum, value) => sum + value, 0) / data.length;
const stdDev = getStandardDeviation(data, mean);

const inferiorLimit = mean - stdDev;
const superiorLimit = mean + stdDev;

return {
belowAverageMin: isInteger
? Math.min(...data)
: roundToTwoDecimals(Math.min(...data)),
belowAverageMax: isInteger
? Math.floor(inferiorLimit)
: roundToTwoDecimals(inferiorLimit, 'floor'),
averageMin: isInteger
? Math.ceil(inferiorLimit)
: roundToTwoDecimals(inferiorLimit, 'ceil'),
averageMax: isInteger
? Math.floor(superiorLimit)
: roundToTwoDecimals(superiorLimit, 'floor'),
aboveAverageMin: isInteger
? Math.ceil(superiorLimit)
: roundToTwoDecimals(superiorLimit, 'ceil'),
aboveAverageMax: isInteger
? Math.max(...data)
: roundToTwoDecimals(Math.max(...data)),
};
};

type Hit = {
objectID: string;
};

type UserProductivityHit = Hit & {
asapOutput: number;
asapPublicOutput: number;
ratio: string;
};

type TeamProductivityHit = Hit & {
[documentType in TeamOutputDocumentType]: number;
};

export const getAllHits = async <T>(
getPaginatedHits: (page: number) => Promise<SearchResponse<T>>,
): Promise<T[]> => {
let page = 0;
let totalPages = 0;
let allHits: T[] = [];

/* eslint-disable no-await-in-loop */
do {
const result = await getPaginatedHits(page);
allHits = allHits.concat(result.hits);
if (totalPages === 0) {
totalPages = result.nbPages;
}
page += 1;
} while (page < totalPages);
/* eslint-enable no-await-in-loop */

return allHits;
};

export const deletePreviousObjects = async (
index: SearchIndex,
type: 'user-productivity' | 'team-productivity',
) => {
const previous = await index.search('', {
filters: `__meta.type:"${type}-performance"`,
});
const objectIDs = previous.hits.map(({ objectID }) => objectID);
await index.deleteObjects(objectIDs);
};

export const processUserProductivityPerformance = async (
index: SearchIndex,
) => {
const type = 'user-productivity' as const;
await deletePreviousObjects(index, type);

timeRanges.forEach(async (range) => {
const getPaginatedHits = (page: number) =>
index.search<UserProductivityHit>('', {
filters: `__meta.range:"${range}" AND (__meta.type:"${type}")`,
attributesToRetrieve: ['asapOutput', 'asapPublicOutput', 'ratio'],
page,
hitsPerPage: 50,
});

const userProductivityHits =
await getAllHits<UserProductivityHit>(getPaginatedHits);

const fields = ['asapOutput', 'asapPublicOutput', 'ratio'];

const userPerformance = fields.reduce(
(metrics, field) => {
if (field === 'ratio') {
return {
...metrics,
ratio: getBellCurveMetrics(
userProductivityHits.map((hit) => parseFloat(hit.ratio)),
false,
),
};
}

return {
...metrics,
[field]: getBellCurveMetrics(
userProductivityHits.map(
(hit) => hit[field as 'asapOutput' | 'asapPublicOutput'],
),
),
};
},
{} as Record<string, PerformanceMetrics>,
);

await index.saveObject(
{
...userPerformance,
__meta: {
range,
type: `${type}-performance`,
},
},
{ autoGenerateObjectIDIfNotExist: true },
);
});
};

export const processTeamProductivityPerformance = async (
index: SearchIndex,
) => {
const type = 'team-productivity' as const;

await deletePreviousObjects(index, type);

timeRanges.forEach(async (range) => {
const getPaginatedHits = (page: number) =>
index.search<TeamProductivityHit>('', {
filters: `__meta.range:"${range}" AND (__meta.type:"${type}")`,
attributesToRetrieve: teamOutputDocumentTypes,
page,
hitsPerPage: 50,
});

const teamProductivityHits =
await getAllHits<TeamProductivityHit>(getPaginatedHits);

const fields = [
{ name: 'Article', documentType: 'article' },
{ name: 'Bioinformatics', documentType: 'bioinformatics' },
{ name: 'Dataset', documentType: 'dataset' },
{ name: 'Lab Resource', documentType: 'labResource' },
{ name: 'Protocol', documentType: 'protocol' },
];

const teamPerformanceByDocumentType = fields.reduce(
(metrics, { name, documentType }) => ({
...metrics,
[documentType]: getBellCurveMetrics(
teamProductivityHits.map(
(hit) => hit[name as TeamOutputDocumentType],
),
),
}),
{} as Record<string, PerformanceMetrics>,
);

await index.saveObject(
{
...teamPerformanceByDocumentType,
__meta: {
range,
type: `${type}-performance`,
},
},
{ autoGenerateObjectIDIfNotExist: true },
);
});
};

export type ProcessProductivityPerformance = {
algoliaAppId: string;
algoliaCiApiKey: string;
indexName: string;
metric: 'all' | 'user-productivity' | 'team-productivity';
};

/* istanbul ignore next */
export const processProductivityPerformance = async ({
algoliaAppId,
algoliaCiApiKey,
indexName,
metric,
}: ProcessProductivityPerformance): Promise<void> => {
const client = algoliasearch(algoliaAppId, algoliaCiApiKey);
const index = client.initIndex(indexName);

if (metric === 'all' || metric === 'user-productivity') {
await processUserProductivityPerformance(index);
}

if (metric === 'all' || metric === 'team-productivity') {
await processTeamProductivityPerformance(index);
}
};
Loading

0 comments on commit e4d5d2b

Please sign in to comment.