generated from chiffre-io/template-library
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extracted code from monorepo
- Loading branch information
Showing
4 changed files
with
307 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters