From 02cfc70424e0647570928ba51b9748ace8315ccb Mon Sep 17 00:00:00 2001
From: Hugo SUBTIL <ext.sopra.husubtil@grandlyon.com>
Date: Tue, 27 Jul 2021 12:58:22 +0000
Subject: [PATCH] feat(dacc): add indicators sending to cozy dacc.

---
 .gitignore                                    |    1 +
 .gitlab-ci.yml                                |    2 +-
 app.config.alpha.js                           |   35 +
 app.config.environment.alpha.js               |   32 +
 app.config.environment.dev.js                 |    1 +
 app.config.js                                 |    2 +-
 manifest.webapp                               |   25 +
 package.json                                  |    3 +
 scripts/server.js                             |   49 +
 src/enum/dacc.enum.ts                         |   12 +
 src/enum/usageEvent.enum.ts                   |   34 +-
 src/enum/usageEventProperties.enum.ts         |    5 +
 src/services/environement.service.spec.ts     |   34 +
 src/services/environement.service.ts          |   12 +
 src/targets/services/aggregatorUsageEvents.ts | 1362 +++++++++++------
 15 files changed, 1146 insertions(+), 463 deletions(-)
 create mode 100644 app.config.alpha.js
 create mode 100644 app.config.environment.alpha.js
 create mode 100644 scripts/server.js
 create mode 100644 src/enum/dacc.enum.ts
 create mode 100644 src/enum/usageEventProperties.enum.ts
 create mode 100644 src/services/environement.service.spec.ts
 create mode 100644 src/services/environement.service.ts

diff --git a/.gitignore b/.gitignore
index 0be88ea48..2fe541dde 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 32275ff12..7054ea3eb 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 000000000..8c1500828
--- /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 000000000..fc29ceef5
--- /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 893510615..c1c0f9834 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 f4a8224da..ab21d2835 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 c215b0a7b..fc8c4c31a 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 101498e55..de5c46681 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 000000000..f36c086ed
--- /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 000000000..61979b712
--- /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 66a5ecc88..0c5825c02 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 000000000..ebeee1bf1
--- /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 000000000..c59e2c104
--- /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 000000000..338883f0c
--- /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 c5ad07b4d..e723060ed 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)
-- 
GitLab