Skip to content

Commit

Permalink
ASAP-537 Algolia for team collaboration performance analytics (#4311)
Browse files Browse the repository at this point in the history
  • Loading branch information
lctrt authored Jun 25, 2024
1 parent fb15fca commit bb1db86
Show file tree
Hide file tree
Showing 13 changed files with 572 additions and 235 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/reusable-crn-analytics-algolia-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ jobs:
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
if: ${{ inputs.metric == 'all' || inputs.metric == 'team-productivity' || inputs.metric == 'user-productivity' || inputs.metric == 'team-collaboration' }}
run: yarn algolia:process-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 }}
Expand Down
2 changes: 1 addition & 1 deletion apps/asap-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +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:process-performance": "ts-node --transpile-only src/cli.ts algolia:process-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
4 changes: 2 additions & 2 deletions apps/asap-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ 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',
command: 'algolia:process-performance',
describe: 'process analytics performance',
builder: (cli) =>
cli
.option('appid', appIdOption)
Expand Down
2 changes: 1 addition & 1 deletion apps/asap-cli/src/scripts/algolia/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +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 './process-performance';
export * from './remove-records';
export * from './set-settings';
export * from './set-analytics-settings';
86 changes: 86 additions & 0 deletions apps/asap-cli/src/scripts/algolia/performance/collaboration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { SearchIndex } from 'algoliasearch';
import {
timeRanges,
TeamOutputDocumentType,
PerformanceMetrics,
} from '@asap-hub/model';

import {
deletePreviousObjects,
getAllHits,
getBellCurveMetrics,
Hit,
} from '../process-performance';

type DocumentMetric = {
Article: number;
Bioinformatics: number;
Dataset: number;
'Lab Resource': number;
Protocol: number;
};

type TeamCollaborationHit = Hit & {
outputsCoProducedAcross: {
byDocumentType: DocumentMetric;
};
outputsCoProducedWithin: DocumentMetric;
};

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

await deletePreviousObjects(index, type);

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

const hits = await getAllHits<TeamCollaborationHit>(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 = (rawMetrics: DocumentMetric[]) =>
fields.reduce(
(metrics, { name, documentType }) => ({
...metrics,
[documentType]: getBellCurveMetrics(
rawMetrics.map((item) => item[name as TeamOutputDocumentType]),
),
}),
{} as Record<string, PerformanceMetrics>,
);

await index.saveObject(
{
withinTeam: teamPerformanceByDocumentType(
hits.map((hit) => hit.outputsCoProducedWithin),
),
accrossTeam: teamPerformanceByDocumentType(
hits.map((hit) => hit.outputsCoProducedAcross.byDocumentType),
),
__meta: {
range,
type: `${type}-performance`,
},
},
{ autoGenerateObjectIDIfNotExist: true },
);
});
};
Original file line number Diff line number Diff line change
@@ -1,75 +1,16 @@
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;
};
import {
deletePreviousObjects,
getAllHits,
getBellCurveMetrics,
Hit,
} from '../process-performance';

type UserProductivityHit = Hit & {
asapOutput: number;
Expand All @@ -81,38 +22,6 @@ 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,
) => {
Expand Down Expand Up @@ -221,29 +130,3 @@ export const processTeamProductivityPerformance = async (
);
});
};

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 bb1db86

Please sign in to comment.