diff --git a/.gitignore b/.gitignore index 0be88ea4811a1014daf8a38bdacfc6872230fe6b..2fe541dde5c14c962135ed21d64c1105f2263ae2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ yarn-error.log scripts/AAAA* scripts/config.js scripts/data/ +.token.json # Report public/report.html diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 32275ff12d10e610d174305353db913b418301a4..7054ea3eb7573a98f2b9da29dbcfd811248820e4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -53,7 +53,7 @@ build-dev: - apk add bash script: - yarn - - yarn build + - yarn build-dev - git config --global user.name build-pipeline - git config --global user.email "$GIT_USER" - git config --global user.password "$GIT_PWD" diff --git a/app.config.alpha.js b/app.config.alpha.js new file mode 100644 index 0000000000000000000000000000000000000000..8c15008280eb5d8832a309168b6f4f79d431d6c4 --- /dev/null +++ b/app.config.alpha.js @@ -0,0 +1,35 @@ +'use strict' + +/** + * This file overrides the default cozy webpack config to specify a custom react config. + * This react config enables the use of TypeScript. + * + * @override node_modules/cozy-scripts/config/webpack.bundle.default.js + */ + +const merge = require('webpack-merge') +const { target, addAnalyzer } = require('cozy-scripts/config/webpack.vars') + +const configs = [ + require('cozy-scripts/config/webpack.config.base'), + require('cozy-scripts/config/webpack.config.chunks'), + //require('cozy-scripts/config/webpack.config.react'), + require('./app.config.react'), // Override the react config + require('cozy-scripts/config/webpack.config.cozy-ui'), + require('cozy-scripts/config/webpack.config.cozy-ui.react'), + require('cozy-scripts/config/webpack.config.intents'), + require('cozy-scripts/config/webpack.config.public'), + require('cozy-scripts/config/webpack.config.pictures'), + require('cozy-scripts/config/webpack.config.vendors'), + require('cozy-scripts/config/webpack.config.manifest'), + require('cozy-scripts/config/webpack.config.progress'), + addAnalyzer ? require('cozy-scripts/config/webpack.config.analyzer') : null, + // require('cozy-scripts/config/webpack.config.services'), + require('./app.config.services'), // Override the services config + require(`cozy-scripts/config/webpack.target.${target}`), +] + +configs.push(require('./app.config.environment.alpha')) + +//module.exports = merge.apply(null, configs) +module.exports = [merge.apply(null, configs)] // cozy builder expects an array diff --git a/app.config.environment.alpha.js b/app.config.environment.alpha.js new file mode 100644 index 0000000000000000000000000000000000000000..fc29ceef5f481c76b058ae5fe9b5f30f989ee8d3 --- /dev/null +++ b/app.config.environment.alpha.js @@ -0,0 +1,32 @@ +'use strict' + +const webpack = require('webpack') +const TerserPlugin = require('terser-webpack-plugin') + +const { target } = require('cozy-scripts/config/webpack.vars') + +module.exports = { + mode: 'none', + plugins: [ + // use a hash as chunk id to avoid id changes of not changing chunk + new webpack.HashedModuleIdsPlugin(), + new webpack.optimize.OccurrenceOrderPlugin(), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('development'), // to compile on production mode (redux) + __DEVELOPMENT__: false, + __DEVTOOLS__: false, + __STACK_ASSETS__: target !== 'mobile', + }), + ], + optimization: { + minimizer: [ + new TerserPlugin({ + parallel: true, + //To fix a SAfari 10 bug : https://github.com/zeit/next.js/issues/5630 + terserOptions: { + safari10: true, + }, + }), + ], + }, +} diff --git a/app.config.environment.dev.js b/app.config.environment.dev.js index 89351061569e9cdffa4006144c6bf1dcb23f1a31..c1c0f983402d26d917645e9e4d9ceb91d7f39ced 100644 --- a/app.config.environment.dev.js +++ b/app.config.environment.dev.js @@ -17,6 +17,7 @@ let plugins = [ const stackProvidedLibsConfig = { plugins: [ new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('development'), __STACK_ASSETS__: true, }), ], diff --git a/app.config.js b/app.config.js index f4a8224dac96eb7356a75268f27bca5e5db67270..ab21d2835e48b5b0fed0889f79e3ab55bcb8ccab 100644 --- a/app.config.js +++ b/app.config.js @@ -30,7 +30,7 @@ const configs = [ addAnalyzer ? require('cozy-scripts/config/webpack.config.analyzer') : null, // require('cozy-scripts/config/webpack.config.services'), require('./app.config.services'), // Override the services config - require(`cozy-scripts/config/webpack.target.${target}`) + require(`cozy-scripts/config/webpack.target.${target}`), ] if (environment === 'production') { diff --git a/manifest.webapp b/manifest.webapp index c215b0a7beec02c399ede08b4bab7f50d5078d2e..fc8c4c31ade5b8f476dbeee8de322ad4b886bd8f 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -41,6 +41,12 @@ }, "settings": { "description": "Requis pour afficher les paramètres Cozy dans la barre Cozy." + }, + "dacc-dev": { + "description": "Requis pour envoyer des statistiques d'utilisation anonymisées." + }, + "dacc": { + "description": "Requis pour envoyer des statistiques d'utilisation anonymisées." } } }, @@ -77,6 +83,12 @@ }, "settings": { "description": "Required to display Cozy settings in the Cozy bar." + }, + "dacc-dev": { + "description": "Required for sending cozy anonymized stats." + }, + "dacc": { + "description": "Required for sending cozy anonymized stats." } } } @@ -98,6 +110,11 @@ "type": "node", "file": "services/monthlyReportNotification/ecolyo.js", "trigger": "@cron 0 0 10 3 * *" + }, + "aggregatorUsageEvents": { + "type": "node", + "file": "services/aggregatorUsageEvents/ecolyo.js", + "trigger": "@cron 0 1 * * *" } }, "permissions": { @@ -132,6 +149,14 @@ "settings": { "type": "io.cozy.settings", "verbs": ["GET"] + }, + "dacc": { + "type": "cc.cozycloud.dacc", + "verbs": ["ALL"] + }, + "dacc-dev": { + "type": "cc.cozycloud.dacc.dev", + "verbs": ["ALL"] } } } diff --git a/package.json b/package.json index 101498e55290ebd8b0d368859bc06c5ecf641ede..de5c4668147fc30212c4b17d46431ba2c7c655a3 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "lint:styles": "stylint src/styles --config ./.stylintrc", "prebuild": "yarn lint", "build:cs": "build:browser", + "build-dev:browser": "cs build --browser --config app.config.alpha.js", + "build-dev:mobile": "cs build --mobile --config app.config.alpha.js", "build:browser": "cs build --browser ", "build:mobile": "cs build --mobile", "watch": "yarn watch:browser", @@ -23,6 +25,7 @@ "watch:css": "yarn run build:css && node-sass -c -w ./src/styles -o src/styles", "start": "npm-run-all -p start:cs", "build": "yarn run build:css && yarn run build:browser", + "build-dev": "yarn run build:css && yarn run build-dev:browser", "release": "standard-version --no-verify", "winstack": "start powershell -command docker run --rm -it -p 8080:8080 -p 5984:5984 -p 8025:8025 -v $PWD/build:/data/cozy-app/ecolyo -v $PWD/data:/usr/local/couchdb/data -v $PWD/docker/disableCSP.yaml:/etc/cozy/cozy.yaml registry.forge.grandlyon.com/web-et-numerique/llle_project/ecolyo/cozy-env", "linuxstack": "docker run --rm -it -p 8080:8080 -p 5984:5984 -p 8025:8025 -v $(pwd)/build:/data/cozy-app/ecolyo -v ~/cozy/data/db:/usr/local/couchdb/data -v ~/cozy/data/storage:/data/cozy-storage -v $(pwd)/docker/disableCSP.yaml:/etc/cozy/cozy.yaml registry.forge.grandlyon.com/web-et-numerique/llle_project/ecolyo/cozy-env", diff --git a/scripts/server.js b/scripts/server.js new file mode 100644 index 0000000000000000000000000000000000000000..f36c086ed29ae13b90654dfbd26074bde88a6dcc --- /dev/null +++ b/scripts/server.js @@ -0,0 +1,49 @@ +const http = require('http') + +const process = (request, response) => { + setTimeout(() => { + response.end() + }, 100) +} + +console.log('Listening on port 8081...') +http + .createServer(function(request, response) { + const requestStart = Date.now() + + let errorMessage = null + let body = [] + request.on('data', chunk => { + body.push(chunk) + }) + request.on('end', () => { + body = Buffer.concat(body) + body = body.toString() + }) + request.on('error', error => { + errorMessage = error.message + }) + + response.on('finish', () => { + const { rawHeaders, httpVersion, method, socket, url } = request + const { remoteAddress, remoteFamily } = socket + + // console.log( + // JSON.stringify({ + // timestamp: Date.now(), + // processingTime: Date.now() - requestStart, + // rawHeaders, + // errorMessage, + // httpVersion, + // method, + // remoteAddress, + // remoteFamily, + // url, + // }) + // ) + console.log(JSON.parse(body)) + }) + + process(request, response) + }) + .listen(8081) diff --git a/src/enum/dacc.enum.ts b/src/enum/dacc.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..61979b712e9277ffc2f76d9fc8403b7443fb54e7 --- /dev/null +++ b/src/enum/dacc.enum.ts @@ -0,0 +1,12 @@ +export enum DaccEvent { + CONNECTION_COUNT_DAILY = 'connection-count-daily', + KONNECTOR_EVENT_DAILY = 'konnector-event-daily', + KONNECTOR_CONNECTED_PER_DAY = 'konnector-connected-daily', + NAVIGATION_COUNT_DAILY = 'navigation-count-daily', + CHALLENGE_LAUNCH_EVENT_DAILY = 'challenge-launch-daily', + CONSUMPTION_VARIATION_MONTHLY = 'consumption-variation-monthly', + SESSION_DURATION = 'session-duration', + NAVIGATION_ACTION_DAILY = 'navigation-action-daily', + EVENT_DURATION = 'event-duration', + QUIZ_STARS = 'quiz-stars', +} diff --git a/src/enum/usageEvent.enum.ts b/src/enum/usageEvent.enum.ts index 66a5ecc88fb670fb2d59ff8a035df31a8d78d06f..0c5825c02d033a9e4d37877c7d7c89c86e0c1321 100644 --- a/src/enum/usageEvent.enum.ts +++ b/src/enum/usageEvent.enum.ts @@ -1,17 +1,17 @@ -export enum UsageEventType { - CONNECTION_EVENT = 'ConnectionEvent', - KONNECTOR_CONNECT_EVENT = 'KonnectorConnectEvent', - KONNECTOR_REFRESH_EVENT = 'KonnectorRefreshEvent', - NAVIGATION_EVENT = 'NavigationEvent', - CONSUMPTION_COMPARE_EVENT = 'ConsumptionCompareEvent', - CONSUMPTION_CHANGE_TIMESTEP_EVENT = 'ConsumptionChangeTimeStepEvent', - CHALLENGE_LAUNCH_EVENT = 'ChallengeLaunchEvent', - CHALLENGE_END_EVENT = 'ChallengeEndEvent', - DUEL_LAUNCH_EVENT = 'DuelLaunchEvent', - DUEL_END_EVENT = 'DuelEndEvent', - QUIZ_END_EVENT = 'QuizEndEvent', - EXPLORATION_END_EVENT = 'ExplorationEndEvent', - ACTION_CHANGE_EVENT = 'ActionChangeEvent', - ACTION_END_EVENT = 'ActionEndEvent', - PROFILE_SET_EVENT = 'ProfileSetEvent', -} +export enum UsageEventType { + CONNECTION_EVENT = 'ConnectionEvent', + KONNECTOR_CONNECT_EVENT = 'KonnectorConnectEvent', + KONNECTOR_REFRESH_EVENT = 'KonnectorRefreshEvent', + NAVIGATION_EVENT = 'NavigationEvent', + CONSUMPTION_COMPARE_EVENT = 'ConsumptionCompareEvent', + CONSUMPTION_CHANGE_TIMESTEP_EVENT = 'ConsumptionChangeTimeStepEvent', + CHALLENGE_LAUNCH_EVENT = 'ChallengeLaunchEvent', + CHALLENGE_END_EVENT = 'ChallengeEndEvent', + DUEL_LAUNCH_EVENT = 'DuelLaunchEvent', + DUEL_END_EVENT = 'DuelEndEvent', + QUIZ_END_EVENT = 'QuizEndEvent', + EXPLORATION_END_EVENT = 'ExplorationEndEvent', + ACTION_CHANGE_EVENT = 'ActionChangeEvent', + ACTION_END_EVENT = 'ActionEndEvent', + PROFILE_SET_EVENT = 'ProfileSetEvent', +} diff --git a/src/enum/usageEventProperties.enum.ts b/src/enum/usageEventProperties.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..ebeee1bf163ef528287686f3aac857a04476d38b --- /dev/null +++ b/src/enum/usageEventProperties.enum.ts @@ -0,0 +1,5 @@ +export enum UsageEventProperties { + TARGET = 1, + RESULT = 2, + CONTEXT = 3, +} diff --git a/src/services/environement.service.spec.ts b/src/services/environement.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c59e2c1049fbf857c81c03ea8bcc4b8080f11626 --- /dev/null +++ b/src/services/environement.service.spec.ts @@ -0,0 +1,34 @@ +import EnvironementService from './environement.service' + +describe('Environement service', () => { + const OLD_ENV = process.env + beforeEach(() => { + jest.resetModules() // Most important - it clears the cache + process.env = { ...OLD_ENV } // Make a copy + }) + + afterAll(() => { + process.env = OLD_ENV // Restore old environment + }) + const environementService = new EnvironementService() + + describe('isProduction method', () => { + it('should return true', async () => { + process.env.NODE_ENV = 'production' + const result = environementService.isProduction() + expect(result).toEqual(true) + }) + + it('should return false, developement case', async () => { + process.env.NODE_ENV = 'development' + const result = environementService.isProduction() + expect(result).toEqual(false) + }) + + it('should return false, non existing case', async () => { + process.env.NODE_ENV = 'toto' + const result = environementService.isProduction() + expect(result).toEqual(false) + }) + }) +}) diff --git a/src/services/environement.service.ts b/src/services/environement.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..338883f0ca333763d2e50d5c9996d70eec2d695f --- /dev/null +++ b/src/services/environement.service.ts @@ -0,0 +1,12 @@ +export default class EnvironementService { + private getEnvironement() { + return process.env.NODE_ENV + } + + public isProduction() { + if (this.getEnvironement() === 'production') { + return true + } + return false + } +} diff --git a/src/targets/services/aggregatorUsageEvents.ts b/src/targets/services/aggregatorUsageEvents.ts index c5ad07b4dfa22bf8aa34a033df8c6eeac1979878..e723060ed2f4873d7d80eada7c92e728653f5874 100644 --- a/src/targets/services/aggregatorUsageEvents.ts +++ b/src/targets/services/aggregatorUsageEvents.ts @@ -1,444 +1,918 @@ -import logger from 'cozy-logger' -import { Client } from 'cozy-client' -import { runService } from './service' -import UsageEventService from 'services/usageEvent.service' -import { FluidStatus, UsageEvent } from 'models' -import { UsageEventType } from 'enum/usageEvent.enum' -import { DateTime } from 'luxon' -import { toNumber, uniq } from 'lodash' -import FluidService from 'services/fluid.service' -import { FluidState, FluidType } from 'enum/fluid.enum' - -const log = logger.namespace('aggregatorUsageEvents') - -interface AggregatorUsageEventsProps { - client: Client -} - -interface Indicator { - source: string - name: string - startDate: string - endDate: string - value: number - groups: { [key: string]: string | boolean } -} - -// Store all id of events read during the process -const readUsageEvents: UsageEvent[] = [] - -const sendIndicator = async ( - indicator: Indicator, - client: Client -): Promise<boolean> => { - try { - await client - .getStackClient() - .fetchJSON('POST', '/remote/org.ecolyo.usage', { - data: JSON.stringify(indicator), - }) - return true - } catch (error) { - log( - 'info', - `Error while sending indicator to remote doctype: ${error.message}` - ) - return false - } -} - -const sendAggregatedEventByDay = async ( - events: UsageEvent[], - eventType: UsageEventType, - client: Client -) => { - const reducedEvents = events.reduce(function( - acc: { - [key: string]: UsageEvent[] - }, - event: UsageEvent - ) { - const key = `${event.eventDate.startOf('day').toString()}|${event.target}|${ - event.result - }|${event.context}` - if (!acc[key]) { - acc[key] = [] - } - acc[key].push(event) - readUsageEvents.push(event) - return acc - }, {}) - for (const item of Object.keys(reducedEvents)) { - const splitedKey = item.split('|') - let group: { [key: string]: string } = {} - if (splitedKey[1] !== 'undefined') { - group = { - ...group, - target: splitedKey[1], - } - } - if (splitedKey[2] !== 'undefined') { - group = { - ...group, - result: splitedKey[2], - } - } - if (splitedKey[3] !== 'undefined') { - group = { - ...group, - context: splitedKey[3], - } - } - const indicator: Indicator = { - source: 'ecolyo', - name: eventType, - startDate: splitedKey[0], - endDate: splitedKey[0], - value: reducedEvents[item].length, - groups: group, - } - const result = await sendIndicator(indicator, client) - if (result) { - readUsageEvents.push(...reducedEvents[item]) - } - } -} - -const sendNoAggregatedEvent = async ( - events: UsageEvent[], - eventType: UsageEventType, - useOfStartDate: boolean, - client: Client -) => { - for (const event of events) { - let startDate: string | null - let endDate: string | null - if (useOfStartDate) { - startDate = event.startDate - ? event.startDate.toString() - : event.eventDate.toString() - endDate = event.eventDate.toString() - } else { - startDate = event.eventDate.startOf('day').toString() - endDate = event.eventDate.startOf('day').toString() - } - let group: { [key: string]: string } = {} - if (event.target) { - group = { - ...group, - target: event.target, - } - } - if (event.result) { - group = { - ...group, - result: event.result, - } - } - if (event.context) { - group = { - ...group, - context: event.context, - } - } - const indicator: Indicator = { - source: 'ecolyo', - name: eventType, - startDate: startDate, - endDate: endDate, - value: 1, - groups: group, - } - const result = await sendIndicator(indicator, client) - if (result) { - readUsageEvents.push(event) - } - } -} - -const calculSessionTime = async (events: UsageEvent[], client: Client) => { - let startSessionDate: DateTime | null = null - let isFirstConnection = false - let navigationCount = 0 - for (const [index, event] of events.entries()) { - if (event.type === UsageEventType.CONNECTION_EVENT) { - if ( - startSessionDate && - index > 0 && - events[index - 1].type !== UsageEventType.CONNECTION_EVENT - ) { - const sessionIndicator: Indicator = { - source: 'ecolyo', - name: isFirstConnection ? 'FirstSession' : 'Session', - startDate: startSessionDate.toString(), - endDate: events[index - 1].eventDate.toString(), - value: navigationCount, - groups: {}, - } - await sendIndicator(sessionIndicator, client) - navigationCount = 0 - } - startSessionDate = event.eventDate - isFirstConnection = - event.result && event.result === 'firstConnection' ? true : false - } else if (event.type === UsageEventType.NAVIGATION_EVENT) { - if (startSessionDate) { - navigationCount += 1 - } - } else if (index === events.length - 1) { - if (startSessionDate) { - const sessionIndicator: Indicator = { - source: 'ecolyo', - name: isFirstConnection ? 'FirstSession' : 'Session', - startDate: startSessionDate.toString(), - endDate: event.eventDate.toString(), - value: navigationCount, - groups: {}, - } - await sendIndicator(sessionIndicator, client) - } - } - } -} - -const calculPeriodBetweenFirstConnectionAndFirstChallenge = async ( - events: UsageEvent[], - firstConnectionEvent: UsageEvent, - client: Client -) => { - const challengeLaunchEvents: UsageEvent[] = events.filter( - (event: UsageEvent) => event.type === UsageEventType.CHALLENGE_LAUNCH_EVENT - ) - if ( - challengeLaunchEvents.length > 0 && - challengeLaunchEvents[0].target === 'CHALLENGE0001' - ) { - const periodIndicator: Indicator = { - source: 'ecolyo', - name: 'PeriodBetweenFirstConnectionAndFirstChallenge', - startDate: firstConnectionEvent.eventDate.toString(), - endDate: challengeLaunchEvents[0].eventDate.toString(), - value: challengeLaunchEvents[0].eventDate.diff( - firstConnectionEvent.eventDate - ).seconds, - groups: {}, - } - await sendIndicator(periodIndicator, client) - } -} - -const calculPeriodBetweenChallenge = async ( - events: UsageEvent[], - client: Client -) => { - const challengeLaunchEvents: UsageEvent[] = events.filter( - (event: UsageEvent) => event.type === UsageEventType.CHALLENGE_LAUNCH_EVENT - ) - if (challengeLaunchEvents.length > 0) { - const allEndedChallengeEvents: UsageEvent[] = await UsageEventService.getEvents( - client, - { - type: UsageEventType.CHALLENGE_END_EVENT, - } - ) - for (const event of challengeLaunchEvents) { - if (event.target && event.target !== 'CHALLENGE0001') { - const challengeId: number = toNumber( - event.target.substr(event.target.length - 4) - ) - const prevChallengeId = `CHALLENGE${(challengeId - 1) - .toString() - .padStart(4, '0')}` - const previousEndedChallengeIndex: number = allEndedChallengeEvents.findIndex( - (endedEvent: UsageEvent) => endedEvent.target === prevChallengeId - ) - if (previousEndedChallengeIndex > -1) { - const periodChallengeIndicator: Indicator = { - source: 'ecolyo', - name: 'PeriodBetweenChallenges', - startDate: allEndedChallengeEvents[ - previousEndedChallengeIndex - ].eventDate.toString(), - endDate: event.eventDate.toString(), - value: event.eventDate.diff( - allEndedChallengeEvents[previousEndedChallengeIndex].eventDate - ).seconds, - groups: { target: event.target, context: prevChallengeId }, - } - await sendIndicator(periodChallengeIndicator, client) - } - } - } - } -} - -const getDuelLaunchIndicator = async (events: UsageEvent[], client: Client) => { - const duelLaunchEvents: UsageEvent[] = events.filter( - (event: UsageEvent) => event.type === UsageEventType.DUEL_LAUNCH_EVENT - ) - const progressDateEvents: DateTime[] = events - .filter( - (event: UsageEvent) => - event.type === UsageEventType.QUIZ_END_EVENT || - event.type === UsageEventType.ACTION_END_EVENT || - event.type === UsageEventType.EXPLORATION_END_EVENT - ) - .map(event => event.eventDate) - - if (duelLaunchEvents.length > 0) { - for (const duelLaunch of duelLaunchEvents) { - const duelLaunchIndicator: Indicator = { - source: 'ecolyo', - name: 'DuelLaunchEvent', - startDate: duelLaunch.eventDate.toString(), - endDate: duelLaunch.eventDate.toString(), - value: 1, - groups: { - result: progressDateEvents.includes( - duelLaunch.eventDate.startOf('day') - ) - ? true - : false, - target: duelLaunch.target ? duelLaunch.target : '', - context: duelLaunch.context ? duelLaunch.context : '', - }, - } - const result = await sendIndicator(duelLaunchIndicator, client) - - if (result) { - readUsageEvents.push(duelLaunch) - } - } - } -} - -const calculateConnectedKonnectorPerDay = async (client: Client) => { - const fluidService: FluidService = new FluidService(client) - const fluidStatus: FluidStatus[] = await fluidService.getFluidStatus() - const connectedKonnectors = fluidStatus.filter( - fluid => fluid.status === FluidState.DONE - ) - if (connectedKonnectors.length > 0) { - const konnectorfluidTypes: FluidType[] = [] - for (const konnector of connectedKonnectors) { - konnectorfluidTypes.push(konnector.fluidType) - } - const KonnectorConnectedPerDayIndicator: Indicator = { - source: 'ecolyo', - name: 'KonnectorConnectedPerDay', - startDate: DateTime.local() - .setZone('utc', { - keepLocalTime: true, - }) - .startOf('day') - .toISO(), - endDate: DateTime.local() - .setZone('utc', { - keepLocalTime: true, - }) - .startOf('day') - .toISO(), - value: connectedKonnectors.length, - groups: { - electricity: konnectorfluidTypes.includes(FluidType.ELECTRICITY), - gaz: konnectorfluidTypes.includes(FluidType.GAS), - water: konnectorfluidTypes.includes(FluidType.WATER), - }, - } - await sendIndicator(KonnectorConnectedPerDayIndicator, client) - } -} - -const aggregateEvents = async ( - events: UsageEvent[], - eventType: UsageEventType, - firstConnectionEvent: UsageEvent, - client: Client -) => { - switch (eventType) { - case UsageEventType.CONNECTION_EVENT: - case UsageEventType.KONNECTOR_CONNECT_EVENT: - case UsageEventType.KONNECTOR_REFRESH_EVENT: - case UsageEventType.NAVIGATION_EVENT: - case UsageEventType.PROFILE_SET_EVENT: - case UsageEventType.CONSUMPTION_COMPARE_EVENT: - case UsageEventType.CONSUMPTION_CHANGE_TIMESTEP_EVENT: - await sendAggregatedEventByDay(events, eventType, client) - break - case UsageEventType.ACTION_CHANGE_EVENT: - await sendNoAggregatedEvent(events, eventType, false, client) - break - case UsageEventType.CHALLENGE_END_EVENT: - case UsageEventType.QUIZ_END_EVENT: - case UsageEventType.EXPLORATION_END_EVENT: - case UsageEventType.ACTION_END_EVENT: - case UsageEventType.DUEL_END_EVENT: - await sendNoAggregatedEvent(events, eventType, true, client) - break - case UsageEventType.DUEL_LAUNCH_EVENT: - await getDuelLaunchIndicator(events, client) - break - case UsageEventType.CHALLENGE_LAUNCH_EVENT: - await sendAggregatedEventByDay(events, eventType, client) - await calculPeriodBetweenFirstConnectionAndFirstChallenge( - events, - firstConnectionEvent, - client - ) - await calculPeriodBetweenChallenge(events, client) - break - default: - break - } -} - -const AggregatorUsageEvents = async ({ - client, -}: AggregatorUsageEventsProps) => { - log('info', 'Launch service') - const events: UsageEvent[] = await UsageEventService.getEvents(client, { - aggregated: false, - eventDate: { - $lt: DateTime.local() - .setZone('utc', { - keepLocalTime: true, - }) - .startOf('day'), - }, - }) - const [firstConnectionEvent] = await UsageEventService.getEvents(client, { - type: UsageEventType.CONNECTION_EVENT, - result: 'firstConnection', - }) - if (events.length > 0) { - log('info', `Fetching Sessions`) - calculSessionTime(events, client) - calculateConnectedKonnectorPerDay(client) - for (const eventType of Object.values(UsageEventType)) { - log('info', `Fetching ${eventType}`) - const filteredEvents: UsageEvent[] = events.filter( - event => event.type === eventType - ) - if (filteredEvents.length > 0) { - await aggregateEvents( - filteredEvents, - eventType, - firstConnectionEvent, - client - ) - } - } - } - const uniqueReadUsageEvents: UsageEvent[] = uniq(readUsageEvents) - log( - 'info', - `Tag aggregated usage: total of ${uniqueReadUsageEvents.length} events` - ) - await UsageEventService.updateUsageEventsAggregated( - client, - uniqueReadUsageEvents - ) - log('info', 'End of service') -} - -runService(AggregatorUsageEvents) +import logger from 'cozy-logger' +import { Client } from 'cozy-client' +import { runService } from './service' +import UsageEventService from 'services/usageEvent.service' +import ProfileService from 'services/profile.service' +import ConsumptionService from 'services/consumption.service' +import { FluidStatus, PerformanceIndicator, UsageEvent } from 'models' +import { UsageEventType } from 'enum/usageEvent.enum' +import { DateTime } from 'luxon' +import { toNumber, uniq } from 'lodash' +import FluidService from 'services/fluid.service' +import { FluidState, FluidType } from 'enum/fluid.enum' +import { getFluidType } from 'utils/utils' +import { TimeStep } from 'enum/timeStep.enum' +import EnvironementService from 'services/environement.service' +import { DaccEvent } from 'enum/dacc.enum' +import { UsageEventProperties } from 'enum/usageEventProperties.enum' +import ChallengeService from 'services/challenge.service' +import { UserChallengeState } from 'enum/userChallenge.enum' + +const log = logger.namespace('aggregatorUsageEvents') + +interface AggregatorUsageEventsProps { + client: Client +} + +interface Indicator { + createdBy: string + measureName: string + startDate: string + value: number | null + group1?: object + group2?: object + group3?: object +} + +// Store all id of events read during the process +const readUsageEvents: UsageEvent[] = [] + +const sendIndicator = async ( + indicator: Indicator, + client: Client +): Promise<boolean> => { + try { + const environementService = new EnvironementService() + log( + 'info', + environementService.isProduction() + ? 'Sending data to dacc' + : 'Sending data to dacc-dev' + ) + await client + .getStackClient() + .fetchJSON( + 'POST', + environementService.isProduction() + ? '/remote/cc.cozycloud.dacc' + : '/remote/cc.cozycloud.dacc.dev', + { + data: JSON.stringify(indicator), + } + ) + return true + } catch (error) { + log( + 'error', + `Error while sending indicator to remote doctype: ${error.message}` + ) + throw error + } +} + +const reduceEvents = ( + events: UsageEvent[] +): { [key: string]: UsageEvent[] } => { + return events.reduce(function( + acc: { + [key: string]: UsageEvent[] + }, + event: UsageEvent + ) { + const key = `${event.eventDate.startOf('day').toString()}|${event.target}|${ + event.result + }|${event.context}` + if (!acc[key]) { + acc[key] = [] + } + acc[key].push(event) + readUsageEvents.push(event) + return acc + }, {}) +} + +const sendAggregatedEventByDay = async ( + events: UsageEvent[], + eventType: DaccEvent, + client: Client, + groupsKeys: { group1: string; group2?: string; group3?: string }, + groupsIndexes: number[], + customValues?: (string | null)[] +) => { + const reducedEvents = reduceEvents(events) + for (const item of Object.keys(reducedEvents)) { + const splitedKey = item.split('|') + let group1 = {} + let group2 = {} + let group3 = {} + if ( + groupsKeys.group1 && + (groupsIndexes[0] !== 0 || (customValues && customValues[0])) + ) { + group1 = { + [groupsKeys.group1]: + customValues && customValues[0] + ? customValues[0] + : splitedKey[groupsIndexes[0]], + } + } + if ( + groupsKeys.group2 && + (groupsIndexes[1] !== 0 || (customValues && customValues[1])) + ) { + group2 = { + [groupsKeys.group2]: + customValues && customValues[1] + ? customValues[1] + : splitedKey[groupsIndexes[1]], + } + } + if ( + groupsKeys.group3 && + (groupsIndexes[2] !== 0 || (customValues && customValues[2])) + ) { + group3 = { + [groupsKeys.group3]: + customValues && customValues[2] + ? customValues[2] + : splitedKey[groupsIndexes[2]], + } + } + const indicator: Indicator = { + createdBy: 'ecolyo', + measureName: eventType, + startDate: DateTime.fromISO(splitedKey[0]).toISODate(), + value: reducedEvents[item].length, + group1: group1, + ...(Object.keys(group2).length > 0 && { group2: group2 }), + ...(Object.keys(group3).length > 0 && { group3: group3 }), + } + const result = await sendIndicator(indicator, client) + if (result) { + readUsageEvents.push(...reducedEvents[item]) + } + } +} + +const handleQuizStars = async (events: UsageEvent[], client: Client) => { + events.forEach(async event => { + const indicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.QUIZ_STARS, + startDate: event.startDate ? event.startDate.toISODate() : '', + value: parseInt(event.result ? event.result : '0'), + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { challenge_id: event.context ? event.context : '' }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { quiz_id: event.target ? event.target : '' }, + } + const result = await sendIndicator(indicator, client) + if (result) { + readUsageEvents.push(event) + } + }) +} + +const calculSessionTime = async (events: UsageEvent[], client: Client) => { + let startSessionDate: DateTime | null = null + let isFirstConnection = false + let navigationCount = 0 + for (const [index, event] of events.entries()) { + if (event.type === UsageEventType.CONNECTION_EVENT) { + if ( + startSessionDate && + index > 0 && + events[index - 1].type !== UsageEventType.CONNECTION_EVENT + ) { + const endDate = events[index - 1].eventDate + const duration = endDate.diff(startSessionDate, ['seconds']).toObject() + .seconds + const sessionIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.SESSION_DURATION, + startDate: startSessionDate.toISODate(), + value: duration === undefined ? 0 : duration, + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { number_pages: navigationCount.toString() }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { session_type: isFirstConnection ? 'first' : 'any' }, + } + await sendIndicator(sessionIndicator, client) + navigationCount = 0 + } + startSessionDate = event.eventDate + isFirstConnection = + event.result && event.result === 'firstConnection' ? true : false + } else if (event.type === UsageEventType.NAVIGATION_EVENT) { + if (startSessionDate) { + navigationCount += 1 + } + } else if (index === events.length - 1) { + if (startSessionDate) { + const endDate = event.eventDate + const duration = endDate.diff(startSessionDate, ['seconds']).toObject() + .seconds + const sessionIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.SESSION_DURATION, + startDate: startSessionDate.toISODate(), + value: duration === undefined ? 0 : duration, + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { number_pages: navigationCount.toString() }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { session_type: isFirstConnection ? 'first' : 'any' }, + } + await sendIndicator(sessionIndicator, client) + } + } + } +} + +const calculPeriodBetweenFirstConnectionAndFirstChallenge = async ( + events: UsageEvent[], + firstConnectionEvent: UsageEvent, + client: Client +) => { + const challengeLaunchEvents: UsageEvent[] = events.filter( + (event: UsageEvent) => event.type === UsageEventType.CHALLENGE_LAUNCH_EVENT + ) + if ( + challengeLaunchEvents.length > 0 && + challengeLaunchEvents[0].target === 'CHALLENGE0001' + ) { + const periodIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.EVENT_DURATION, + startDate: firstConnectionEvent.eventDate.toISODate(), + value: challengeLaunchEvents[0].eventDate.diff( + firstConnectionEvent.eventDate + ).seconds, + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { start_event: 'first_session' }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { end_event: 'first_challenge' }, + } + const result = await sendIndicator(periodIndicator, client) + if (result) { + readUsageEvents.push(challengeLaunchEvents[0]) + } + } +} + +const calculPeriodBetweenChallenge = async ( + events: UsageEvent[], + client: Client +) => { + const challengeLaunchEvents: UsageEvent[] = events.filter( + (event: UsageEvent) => event.type === UsageEventType.CHALLENGE_LAUNCH_EVENT + ) + if (challengeLaunchEvents.length > 0) { + const allEndedChallengeEvents: UsageEvent[] = await UsageEventService.getEvents( + client, + { + type: UsageEventType.CHALLENGE_END_EVENT, + } + ) + for (const event of challengeLaunchEvents) { + if (event.target && event.target !== 'CHALLENGE0001') { + const challengeId: number = toNumber( + event.target.substr(event.target.length - 4) + ) + const prevChallengeId = `CHALLENGE${(challengeId - 1) + .toString() + .padStart(4, '0')}` + const previousEndedChallengeIndex: number = allEndedChallengeEvents.findIndex( + (endedEvent: UsageEvent) => endedEvent.target === prevChallengeId + ) + if (previousEndedChallengeIndex > -1) { + const periodChallengeIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.EVENT_DURATION, + startDate: allEndedChallengeEvents[ + previousEndedChallengeIndex + ].eventDate.toISODate(), + value: event.eventDate.diff( + allEndedChallengeEvents[previousEndedChallengeIndex].eventDate + ).seconds, + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { start_event: 'first_session' }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { end_event: 'first_challenge' }, + group3: { params: event.target + ':' + prevChallengeId }, + } + await sendIndicator(periodChallengeIndicator, client) + } + } + } + } +} + +/** + * Format a string with all konnectors in success state + * @param konnectorfluidTypes FluidType[] + * @returns string + */ +const getConnectedKonnectorSlug = ( + konnectorfluidTypes: FluidType[] +): string => { + let slug = '' + if (konnectorfluidTypes.includes(FluidType.ELECTRICITY)) { + slug += 'electricity' + } + if (konnectorfluidTypes.includes(FluidType.GAS)) { + if (slug.length > 0) { + slug += ':' + } + slug += 'gas' + } + if (konnectorfluidTypes.includes(FluidType.WATER)) { + if (slug.length > 0) { + slug += ':' + } + slug += 'water' + } + return slug +} + +const calculateConnectedKonnectorPerDay = async (client: Client) => { + const fluidService: FluidService = new FluidService(client) + const fluidStatus: FluidStatus[] = await fluidService.getFluidStatus() + const connectedKonnectors = fluidStatus.filter( + fluid => fluid.status === FluidState.DONE + ) + log('info', 'calculateConnectedKonnectorPerDay') + if (connectedKonnectors.length > 0) { + const konnectorfluidTypes: FluidType[] = [] + for (const konnector of connectedKonnectors) { + konnectorfluidTypes.push(konnector.fluidType) + } + const KonnectorConnectedPerDayIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.KONNECTOR_CONNECTED_PER_DAY, + startDate: DateTime.local() + .setZone('utc', { + keepLocalTime: true, + }) + .startOf('day') + .toISODate(), + value: connectedKonnectors.length, + group1: { categories: getConnectedKonnectorSlug(konnectorfluidTypes) }, + } + await sendIndicator(KonnectorConnectedPerDayIndicator, client) + } +} + +/** + * Build indicator group string with handling of : ECS, heating for gas and electricity. + * + * @param client Client + * @param fluidType FluidType + * @returns Promise<string> + */ +const buildProfileWithFuildType = async ( + client: Client, + fluidType: FluidType +): Promise<string> => { + let formatedProfile = '' + const profile = await new ProfileService(client).getProfile() + // If profile is not filled, return empty string + if (profile && !profile.isProfileTypeCompleted) return formatedProfile + + if (fluidType === FluidType.ELECTRICITY) { + if ( + profile && + profile.profileType && + profile.profileType.warmingFluid === FluidType.ELECTRICITY + ) { + formatedProfile = 'chauffage élec' + } + if ( + profile && + profile.profileType && + profile.profileType.hotWaterFluid === FluidType.ELECTRICITY + ) { + if (formatedProfile.length === 0) { + formatedProfile = 'ECS' + } else { + formatedProfile += ':ECS' + } + } + if ( + profile && + profile.profileType && + profile.profileType.cookingFluid === FluidType.ELECTRICITY + ) { + if (formatedProfile.length === 0) { + formatedProfile = 'cuisine élec' + } else { + formatedProfile += ':cuisine élec' + } + } + if (formatedProfile.length === 0) { + return 'électricité spécifique' + } else { + return formatedProfile + } + } else if (fluidType === FluidType.GAS) { + if ( + profile && + profile.profileType && + profile.profileType.warmingFluid === FluidType.GAS + ) { + formatedProfile = 'chauffage gaz' + } + if ( + profile && + profile.profileType && + profile.profileType.hotWaterFluid === FluidType.GAS + ) { + if (formatedProfile.length === 0) { + formatedProfile = 'ECS' + } else { + formatedProfile += ':ECS' + } + } + if ( + profile && + profile.profileType && + profile.profileType.cookingFluid === FluidType.GAS + ) { + if (formatedProfile.length === 0) { + formatedProfile = 'cuisine gaz' + } else { + formatedProfile += ':cuisine gaz' + } + } + if (formatedProfile.length === 0) { + return 'autre' + } else { + return formatedProfile + } + } + return formatedProfile +} + +const getConsumptionValue = async ( + client: Client, + fluidType: FluidType[] +): Promise<PerformanceIndicator[]> => { + const consumptionService = new ConsumptionService(client) + const analysisDate = DateTime.local().setZone('utc', { keepLocalTime: true }) + const periods = { + timePeriod: { + startDate: analysisDate.minus({ month: 1 }).startOf('month'), + endDate: analysisDate.minus({ month: 1 }).endOf('month'), + }, + comparisonTimePeriod: { + startDate: analysisDate.minus({ month: 2 }).startOf('month'), + endDate: analysisDate.minus({ month: 2 }).endOf('month'), + }, + } + const fetchedPerformanceIndicators = await consumptionService.getPerformanceIndicators( + periods.timePeriod, + TimeStep.MONTH, + fluidType, + periods.comparisonTimePeriod + ) + return fetchedPerformanceIndicators +} + +/** + * Send an indicator on the consumption variation in % for each fluid type. + * @param client + * @group [{ slug }, { seniority (in month) }, { profile (ECS, chauffage, etc...) }], + */ +const calculateConsumptionVariation = async (client: Client) => { + log('info', `calculateConsumptionVariation`) + const consumptionData = await getConsumptionValue(client, [ + FluidType.ELECTRICITY, + FluidType.GAS, + FluidType.WATER, + ]) + for (const fluidType in [ + FluidType.ELECTRICITY, + FluidType.GAS, + FluidType.WATER, + ]) { + if (fluidType < FluidType.MULTIFLUID.toFixed()) { + // Seniority process + const [firstConnectionEvent] = await UsageEventService.getEvents(client, { + type: UsageEventType.CONNECTION_EVENT, + result: 'firstConnection', + }) + + if (firstConnectionEvent) { + const seniority = DateTime.local() + .setZone('utc', { + keepLocalTime: true, + }) + .diff(firstConnectionEvent.eventDate).months + + const consumptionCatiationIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.CONSUMPTION_VARIATION_MONTHLY, + startDate: DateTime.local() + .setZone('utc', { + keepLocalTime: true, + }) + .startOf('day') + .toISODate(), + value: + consumptionData[fluidType] && + consumptionData[fluidType].percentageVariation + ? consumptionData[fluidType].percentageVariation + : 0, // in percent + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { slug: FluidType[fluidType].toLowerCase() }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { seniority: seniority.toString() }, + group3: { + profile: await buildProfileWithFuildType( + client, + getFluidType(FluidType[fluidType]) + ), + }, + } + + // if user wasnt connected during current month, dont send indicator + const events: UsageEvent[] = await UsageEventService.getEvents(client, { + eventDate: { + $lt: DateTime.local() + .setZone('utc', { + keepLocalTime: true, + }) + .endOf('month') + .minus({ month: 1 }) + .toString(), + $gt: DateTime.local() + .setZone('utc', { + keepLocalTime: true, + }) + .startOf('month') + .minus({ month: 1 }) + .toString(), + }, + }) + + if (events.length > 0 || consumptionCatiationIndicator.value !== 0) { + await sendIndicator(consumptionCatiationIndicator, client) + } + } + } + } +} + +const aggregateEvents = async ( + events: UsageEvent[], + eventType: UsageEventType, + firstConnectionEvent: UsageEvent, + client: Client +) => { + let reducedEvents = null // Used to store reducedEvents during calculation + switch (eventType) { + case UsageEventType.CONNECTION_EVENT: + await sendAggregatedEventByDay( + events, + DaccEvent.CONNECTION_COUNT_DAILY, + client, + { group1: 'device' }, + [UsageEventProperties.CONTEXT] + ) + break + case UsageEventType.KONNECTOR_CONNECT_EVENT: + await sendAggregatedEventByDay( + events, + DaccEvent.KONNECTOR_EVENT_DAILY, + client, + { group1: 'slug', group2: 'event_type', group3: 'status' }, + [UsageEventProperties.TARGET, 0, UsageEventProperties.RESULT], + [null, 'connexion', null] + ) + break + case UsageEventType.KONNECTOR_REFRESH_EVENT: + await sendAggregatedEventByDay( + events, + DaccEvent.KONNECTOR_EVENT_DAILY, + client, + { group1: 'slug', group2: 'event_type', group3: 'status' }, + [UsageEventProperties.TARGET, 0, UsageEventProperties.RESULT], + [null, 'refresh', null] + ) + break + case UsageEventType.NAVIGATION_EVENT: + await sendAggregatedEventByDay( + events, + DaccEvent.NAVIGATION_COUNT_DAILY, + client, + { group1: 'page' }, + [UsageEventProperties.TARGET] + ) + break + case UsageEventType.PROFILE_SET_EVENT: + await sendAggregatedEventByDay( + events, + DaccEvent.NAVIGATION_ACTION_DAILY, + client, + { group1: 'page', group2: 'action', group3: 'params' }, + [0, 0, 0], + ['profile', 'profile_completed', ''] + ) + break + case UsageEventType.CONSUMPTION_COMPARE_EVENT: + reducedEvents = reduceEvents(events) + for (const item of Object.keys(reducedEvents)) { + const splitedKey = item.split('|') + const indicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.NAVIGATION_ACTION_DAILY, + startDate: DateTime.fromISO(splitedKey[0]).toISODate(), + value: reducedEvents[item].length, + group1: { + page: 'consumption', + }, + group2: { + feature: 'compare', + }, + group3: { + params: + splitedKey[UsageEventProperties.CONTEXT] + + ':' + + splitedKey[UsageEventProperties.TARGET], + }, + } + const result = await sendIndicator(indicator, client) + if (result) { + readUsageEvents.push(...reducedEvents[item]) + } + } + break + case UsageEventType.CONSUMPTION_CHANGE_TIMESTEP_EVENT: + reducedEvents = reduceEvents(events) + for (const item of Object.keys(reducedEvents)) { + const splitedKey = item.split('|') + const indicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.NAVIGATION_ACTION_DAILY, + startDate: DateTime.fromISO(splitedKey[0]).toISODate(), + value: reducedEvents[item].length, + group1: { + page: 'consumption', + }, + group2: { + feature: 'changestep', + }, + group3: { + params: + splitedKey[UsageEventProperties.CONTEXT] + + ':' + + splitedKey[UsageEventProperties.TARGET], + }, + } + const result = await sendIndicator(indicator, client) + if (result) { + readUsageEvents.push(...reducedEvents[item]) + } + } + break + case UsageEventType.ACTION_CHANGE_EVENT: + reducedEvents = reduceEvents(events) + for (const item of Object.keys(reducedEvents)) { + const splitedKey = item.split('|') + const indicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.NAVIGATION_ACTION_DAILY, + startDate: DateTime.fromISO(splitedKey[0]).toISODate(), + value: reducedEvents[item].length, + group1: { + page: 'action', + }, + group2: { + feature: 'change_ecogesture', + }, + group3: { + params: + splitedKey[UsageEventProperties.TARGET] + + ':' + + splitedKey[UsageEventProperties.CONTEXT], + }, + } + const result = await sendIndicator(indicator, client) + if (result) { + readUsageEvents.push(...reducedEvents[item]) + } + } + break + case UsageEventType.CHALLENGE_END_EVENT: + for (const event of events) { + const duelLaunchIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.EVENT_DURATION, + startDate: event.eventDate.toISODate(), + value: event.startDate + ? event.eventDate.diff(event.startDate, ['seconds']).seconds + : 0, + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { start_event: 'start_challenge' }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { end_event: 'end_challenge' }, + group3: { params: event.target ? event.target : '' }, + } + const result = await sendIndicator(duelLaunchIndicator, client) + + if (result) { + readUsageEvents.push(event) + } + } + break + case UsageEventType.QUIZ_END_EVENT: + handleQuizStars(events, client) + break + case UsageEventType.EXPLORATION_END_EVENT: + for (const event of events) { + const duelLaunchIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.EVENT_DURATION, + startDate: event.eventDate.toISODate(), + value: event.startDate + ? event.eventDate.diff(event.startDate, ['seconds']).seconds + : 0, + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { start_event: 'start_exploration' }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { end_event: 'end_exploration' }, + group3: { params: event.context + ':' + event.target }, + } + const result = await sendIndicator(duelLaunchIndicator, client) + + if (result) { + readUsageEvents.push(event) + } + } + break + case UsageEventType.ACTION_END_EVENT: + for (const event of events) { + const duelLaunchIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.EVENT_DURATION, + startDate: event.eventDate.toISODate(), + value: event.startDate + ? event.eventDate.diff(event.startDate, ['seconds']).seconds + : 0, + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { start_event: 'start_action' }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { end_event: 'end_action' }, + group3: { params: event.target + ':' + event.context }, + } + const result = await sendIndicator(duelLaunchIndicator, client) + + if (result) { + readUsageEvents.push(event) + } + } + break + case UsageEventType.DUEL_END_EVENT: + for (const event of events) { + const duelLaunchIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.EVENT_DURATION, + startDate: event.eventDate.toISODate(), + value: event.startDate + ? event.eventDate.diff(event.startDate, ['seconds']).seconds + : 0, + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { start_event: 'start_duel' }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { end_event: 'end_duel' }, + group3: { + params: event.target + ':' + event.context + ':' + event.result, + }, + } + const result = await sendIndicator(duelLaunchIndicator, client) + + if (result) { + readUsageEvents.push(event) + } + } + break + case UsageEventType.DUEL_LAUNCH_EVENT: + // Get challenge ID + const challenges = await new ChallengeService( + client + ).getAllUserChallengeEntities() + const filteredCurrentChallenge = challenges.filter( + challenge => + challenge.state === UserChallengeState.ONGOING || + challenge.state === UserChallengeState.DUEL + ) + const currentChallenge = filteredCurrentChallenge[0] + ? filteredCurrentChallenge[0] + : null + + let starNumbers = 0 + if (currentChallenge && currentChallenge.progress) { + starNumbers = + currentChallenge.progress.actionProgress + + currentChallenge.progress.explorationProgress + + currentChallenge.progress.quizProgress + } + + for (const event of events) { + const duelLaunchIndicator: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.EVENT_DURATION, + startDate: event.eventDate.toISODate(), + value: event.startDate + ? event.eventDate.diff(event.startDate, ['seconds']).seconds + : 0, + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { start_event: 'start_duel' }, + // eslint-disable-next-line @typescript-eslint/camelcase + group2: { end_event: 'start_duel' }, + group3: { + params: event.target + ':' + event.context + ':' + starNumbers, + }, + } + const result = await sendIndicator(duelLaunchIndicator, client) + + if (result) { + readUsageEvents.push(event) + } + } + break + case UsageEventType.CHALLENGE_LAUNCH_EVENT: + await calculPeriodBetweenFirstConnectionAndFirstChallenge( + events, + firstConnectionEvent, + client + ) + await calculPeriodBetweenChallenge(events, client) + break + default: + break + } +} + +const AggregatorUsageEvents = async ({ + client, +}: AggregatorUsageEventsProps) => { + log('info', 'Launch service') + const events: UsageEvent[] = await UsageEventService.getEvents(client, { + aggregated: false, + eventDate: { + $lt: DateTime.local() + .setZone('utc', { + keepLocalTime: true, + }) + .startOf('day'), + }, + }) + const [firstConnectionEvent] = await UsageEventService.getEvents(client, { + type: UsageEventType.CONNECTION_EVENT, + result: 'firstConnection', + }) + if (events.length > 0) { + log('info', `Fetching Sessions`) + calculSessionTime(events, client) + for (const eventType of Object.values(UsageEventType)) { + log('info', `Fetching ${eventType}`) + const filteredEvents: UsageEvent[] = events.filter( + event => event.type === eventType + ) + if (filteredEvents.length > 0) { + await aggregateEvents( + filteredEvents, + eventType, + firstConnectionEvent, + client + ) + } + } + } + // Daily events + calculateConnectedKonnectorPerDay(client) + // Monthly events + const profile = await new ProfileService(client).getProfile() + if ( + profile && + DateTime.local() + .setZone('utc', { + keepLocalTime: true, + }) + .startOf('day').day === profile.monthlyAnalysisDate.day + ) { + calculateConsumptionVariation(client) + } + const uniqueReadUsageEvents: UsageEvent[] = uniq(readUsageEvents) + log( + 'info', + `Tag aggregated usage: total of ${uniqueReadUsageEvents.length} events` + ) + await UsageEventService.updateUsageEventsAggregated( + client, + uniqueReadUsageEvents + ) + log('info', 'End of service') +} + +runService(AggregatorUsageEvents)