Skip to content

Commit

Permalink
feat: Initial commit
Browse files Browse the repository at this point in the history
Extracted code from monorepo
  • Loading branch information
franky47 committed Mar 14, 2020
1 parent eac3ab8 commit d9d778d
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 14 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# @chiffre/template-library
# @chiffre/analytics-processing

[![NPM](https://img.shields.io/npm/v/@chiffre/template-library?color=red)](https://www.npmjs.com/package/@chiffre/template-library)
[![MIT License](https://img.shields.io/github/license/chiffre-io/template-library.svg?color=blue)](https://github.com/chiffre-io/template-library/blob/next/LICENSE)
[![Continuous Integration](https://github.com/chiffre-io/template-library/workflows/Continuous%20Integration/badge.svg?branch=next)](https://github.com/chiffre-io/template-library/actions)
[![Coverage Status](https://coveralls.io/repos/github/chiffre-io/template-library/badge.svg?branch=next)](https://coveralls.io/github/chiffre-io/template-library?branch=next)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=chiffre-io/template-library)](https://dependabot.com)
[![NPM](https://img.shields.io/npm/v/@chiffre/analytics-processing?color=red)](https://www.npmjs.com/package/@chiffre/analytics-processing)
[![MIT License](https://img.shields.io/github/license/chiffre-io/analytics-processing.svg?color=blue)](https://github.com/chiffre-io/analytics-processing/blob/next/LICENSE)
[![Continuous Integration](https://github.com/chiffre-io/analytics-processing/workflows/Continuous%20Integration/badge.svg?branch=next)](https://github.com/chiffre-io/analytics-processing/actions)
[![Coverage Status](https://coveralls.io/repos/github/chiffre-io/analytics-processing/badge.svg?branch=next)](https://coveralls.io/github/chiffre-io/analytics-processing?branch=next)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=chiffre-io/analytics-processing)](https://dependabot.com)

Template for Chiffre libraries
Transform raw analytics data points into insightful metrics

## License

[MIT](https://github.com/chiffre-io/template-library/blob/next/LICENSE) - Made with ❤️ by [François Best](https://francoisbest.com).
[MIT](https://github.com/chiffre-io/analytics-processing/blob/next/LICENSE) - Made with ❤️ by [François Best](https://francoisbest.com).
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@chiffre/template-library",
"name": "@chiffre/analytis-processing",
"version": "0.0.0-semantically-released",
"description": "Template for Chiffre libraries",
"description": "Transform raw analytics data points into insightful metrics",
"main": "dist/index.js",
"license": "MIT",
"author": {
Expand All @@ -11,11 +11,11 @@
},
"repository": {
"type": "git",
"url": "https://github.com/chiffre-io/template-library"
"url": "https://github.com/chiffre-io/analytis-processing"
},
"keywords": [
"chiffre",
"template"
"analytics"
],
"publishConfig": {
"access": "public"
Expand All @@ -28,7 +28,10 @@
"build": "run-s build:clean build:ts",
"ci": "run-s build test"
},
"dependencies": {},
"dependencies": {
"@chiffre/analytics-core": "^1.0.1",
"bowser": "^2.9.0"
},
"devDependencies": {
"@commitlint/config-conventional": "^8.3.4",
"@types/jest": "^25.1.4",
Expand Down
275 changes: 274 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,274 @@
export default (name: string) => `Hello, ${name} !`
import {
BrowserEvent,
SessionData,
GenericEvent,
isSessionStartEvent,
isGenericStringEvent,
isGenericNumberEvent,
isGenericNumbersEvent,
isGenericStringsEvent,
isBrowserEvent,
isSessionEndEvent
} from '@chiffre/analytics-core'
import Bowser from 'bowser'

export interface LeaderboardEntry<K = string> {
key: K
score: number
percent: number
}

export class CounterMap<K = string> {
private _map: Map<K, number>

constructor() {
this._map = new Map()
}

public count(key: K) {
this._map.set(key, (this._map.get(key) || 0) + 1)
}

public get leaderboard(): LeaderboardEntry<K>[] {
const sum = Array.from(this._map.values()).reduce((s, c) => s + c, 0)
return Array.from(this._map.entries())
.map(([key, count]) => ({
key,
score: count,
percent: (100 * count) / sum
}))
.sort((a, b) => b.score - a.score)
}
}

// --

function findSessionStart(events: BrowserEvent[]) {
return events.find(isSessionStartEvent)
}

export interface NumericStats {
min: number
max: number
avg: number
}

export interface ReturningVisitorInfo<E> {
info: SessionData
timeSinceLastVisit: number
session: E[]
}

export class BrowserEventsProcessor {
private _sessionMap: Map<string, BrowserEvent[]>
browsers: CounterMap
pageCount: CounterMap
referrers: CounterMap
userAgents: CounterMap
lang: CounterMap
os: CounterMap
osWithVersion: CounterMap
viewportWidth: CounterMap<number>

constructor() {
this._sessionMap = new Map()
this.pageCount = new CounterMap()
this.referrers = new CounterMap()
this.userAgents = new CounterMap()
this.os = new CounterMap()
this.osWithVersion = new CounterMap()
this.lang = new CounterMap()
this.viewportWidth = new CounterMap<number>()
this.browsers = new CounterMap()
}

public process(event: BrowserEvent) {
if (!isBrowserEvent(event) || !event.data) {
return
}
const key = event.data.sid
const sessionEvents = this._sessionMap.get(key) || []
this._sessionMap.set(
key,
[...sessionEvents, event].sort((a, b) => a.time - b.time)
)
this.pageCount.count(event.data.path)
if (isSessionStartEvent(event)) {
const ua = Bowser.parse(event.data.ua)
this.referrers.count(event.data.ref)
this.userAgents.count(
`${ua.browser.name} ${ua.browser.version || '(unknown)'}`
)
this.osWithVersion.count(`${ua.os.name} ${ua.os.version || '(unknown)'}`)
this.os.count(ua.os.name || 'N.A.')
this.lang.count(event.data.lang)
this.viewportWidth.count(event.data.vp.w)
this.browsers.count(ua.browser.name || 'N.A.')
}
}

public get sessions(): Map<string, BrowserEvent[]> {
return this._sessionMap
}

public get sessionDurations(): Map<string, number> {
return new Map(
Array.from(this._sessionMap.entries())
.map<[string, number]>(([sid, events]) => {
if (events.length === 0) {
return [sid, 0]
}
// For events sorted by time:
const min = events[0].time
const max = events[events.length - 1].time
// For unsorted events:
// const { min, max } = events.reduce(
// ({ min, max }, event) => ({
// max: Math.max(max, event.time),
// min: Math.min(min, event.time)
// }),
// { min: Infinity, max: 0 }
// )
return [sid, max - min]
})
.sort((a, b) => a[1] - b[1])
)
}

public get returningVisitors(): Map<
string,
ReturningVisitorInfo<BrowserEvent>
> {
return new Map(
Array.from(this.sessions.entries())
.filter(([_sid, events]) => {
// Keep only sessions with a start event with last visited date
const start = findSessionStart(events)
return start && start.data.lvd
})
.map<[string, ReturningVisitorInfo<BrowserEvent>]>(([sid, events]) => {
const start = findSessionStart(events)!
return [
sid,
{
info: start.data,
session: events,
timeSinceLastVisit:
start.time - new Date(start.data.lvd!).valueOf()
}
]
})
)
}

public get timeOnSite(): NumericStats {
if (this.sessions.size === 0) {
return {
min: 0,
max: 0,
avg: 0
}
}
const sessionLengths = Array.from(this.sessions.values()).map(
session => session[session.length - 1].time - session[0].time
)
const { min, max, sum } = sessionLengths.reduce(
({ min, max, sum }, d) => ({
min: Math.min(d, min),
max: Math.max(d, max),
sum: d + sum
}),
{ min: Infinity, max: 0, sum: 0 }
)
return {
min,
max,
avg: sum / sessionLengths.length
}
}

public get timeOnPage(): Map<string, NumericStats> {
const rawDurations = new Map<string, number[]>()
for (const session of this.sessions.values()) {
interface ReduceVisitor {
currentPath?: string
startTime?: number
}
session.reduce<ReduceVisitor>((visitor, event, i) => {
const eventPath = event.data?.path || 'N.A.'
if (i === 0) {
return {
currentPath: eventPath,
startTime: event.time
}
}
if (
event.data!.path === visitor.currentPath ||
isSessionEndEvent(event)
) {
return visitor
}
const key = visitor.currentPath || 'N.A.'
const timeOnPage = event.time - visitor.startTime!
rawDurations.set(key, [...(rawDurations.get(key) || []), timeOnPage])
return {
currentPath: eventPath,
startTime: event.time
}
}, {})
}
return new Map(
Array.from(rawDurations.entries()).map(([path, durations]) => {
const { min, max, sum } = durations.reduce(
({ min, max, sum }, d) => ({
min: Math.min(d, min),
max: Math.max(d, max),
sum: d + sum
}),
{ min: Infinity, max: 0, sum: 0 }
)
return [
path,
{
min,
max,
avg: sum / durations.length
}
]
})
)
}

public get timeOnPageLeaderboard(): LeaderboardEntry[] {
const sum = Array.from(this.timeOnPage.values()).reduce(
(s, c) => s + c.avg,
0
)
return Array.from(this.timeOnPage.entries())
.map(([key, stats]) => ({
key,
score: stats.avg,
percent: (100 * stats.avg) / sum
}))
.sort((a, b) => b.score - a.score)
}
}

// --

export function extractGenericsNames(events: GenericEvent[]) {
const map = new Map<string, GenericEvent[]>()
for (const event of events) {
if (isGenericNumberEvent(event) || isGenericStringEvent(event)) {
const existing = map.get(event.data.name) || []
map.set(event.data.name, [...existing, event])
}
if (isGenericNumbersEvent(event) || isGenericStringsEvent(event)) {
for (const datapoint of event.data) {
const existing = map.get(datapoint.name) || []
map.set(datapoint.name, [...existing, event])
}
}
}
return map
}
17 changes: 17 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==

"@chiffre/analytics-core@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@chiffre/analytics-core/-/analytics-core-1.0.1.tgz#519f49162266f93fb4598c9f058f999b83a958fa"
integrity sha512-OWrXpePCWreNr4ioryRv8YGyCANChiHiyaR+GPcyP+B388sE6a6NXN4Cb7o67Ow2TuVOkxdLlnKNcr+h0Yecxg==
dependencies:
nanoid "^2.1.11"

"@cnakazawa/watch@^1.0.3":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
Expand Down Expand Up @@ -846,6 +853,11 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"

bowser@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9"
integrity sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA==

brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
Expand Down Expand Up @@ -2871,6 +2883,11 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==

nanoid@^2.1.11:
version "2.1.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==

nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
Expand Down

0 comments on commit d9d778d

Please sign in to comment.