Skip to content

Commit

Permalink
Merge pull request #65 from ipfs-shipyard/feat/accumulate-events
Browse files Browse the repository at this point in the history
feat: Accumulate Events
  • Loading branch information
whizzzkid committed Jan 27, 2023
2 parents ee32bf9 + 3549175 commit 2d43252
Show file tree
Hide file tree
Showing 22 changed files with 3,857 additions and 2,266 deletions.
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
root=true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2
12 changes: 9 additions & 3 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@
"sourceType": "module"
},
"rules": {
"sort-imports": ["error"]
},
"overrides": [
{
"files": "test/**/*.spec.ts",
"rules": {
"@typescript-eslint/no-unused-expressions": "off"
"files": [
"test/**/*.ts"
],
"parserOptions": {
"project": "test/tsconfig.json"
},
"env": {
"mocha": true
}
}
]
Expand Down
9 changes: 9 additions & 0 deletions .mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extensions": ["ts"],
"spec": ["test/**/*.spec.*"],
"require": ["ts-node/register"],
"node-option": [
"experimental-specifier-resolution=node",
"loader=ts-node/esm"
]
}
5,621 changes: 3,402 additions & 2,219 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 3 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,6 @@
"import": "./dist/src/*.js"
}
},
"eslintConfig": {
"extends": "ipfs",
"parserOptions": {
"sourceType": "module"
}
},
"release": {
"branches": [
"main"
Expand Down Expand Up @@ -149,9 +143,8 @@
"lint": "aegir lint",
"release": "aegir release",
"build": "aegir build",
"pretest": "run-s build",
"test": "run-s test:*",
"test:node": "aegir test -t node -f 'dist/test/node.spec.js'",
"test:node": "aegir test --target node --files 'test/node/**/*.spec.ts'",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
Expand All @@ -178,6 +171,7 @@
"@storybook/testing-library": "^0.0.13",
"@types/mocha": "^10.0.1",
"@types/react-dom": "^18.0.10",
"@types/sinon": "^10.0.13",
"@types/sinon-chai": "^3.2.9",
"@types/styled-components": "^5.1.26",
"aegir": "^38.1.0",
Expand All @@ -196,6 +190,7 @@
"sinon": "^15.0.1",
"sinon-chai": "^3.7.0",
"style-loader": "^3.3.1",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
"peerDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions src/BrowserMetricsProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Countly from 'countly-sdk-web'
import type { MetricProviderOptionalConstructorArgs, WithOptional } from '../types/index.js'
import MetricsProvider, { MetricsProviderConstructorOptions } from './MetricsProvider.js'
import { BrowserStorageProvider } from './BrowserStorageProvider.js'
import type { MetricProviderOptionalConstructorArgs, WithOptional } from './types/index.js'
import Countly from 'countly-sdk-web'

export class BrowserMetricsProvider extends MetricsProvider<typeof Countly> {
constructor (args: WithOptional<MetricsProviderConstructorOptions<typeof Countly>, MetricProviderOptionalConstructorArgs>) {
Expand Down
2 changes: 1 addition & 1 deletion src/BrowserStorageProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { consentTypes } from './types/index.js'
import type { StorageProvider } from './StorageProvider.js'
import type { consentTypes } from '../types/index.js'

export class BrowserStorageProvider implements StorageProvider {
setStore (consentArray: consentTypes[]): void {
Expand Down
153 changes: 153 additions & 0 deletions src/EventAccumulator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { CountlyEvent, CountlyEventData, CountlyWebSdk, IEventAccumulator } from 'countly-sdk-web'
import type { CountlyNodeSdk } from 'countly-sdk-nodejs'

const eventDefaults: CountlyEventData = {
key: '',
count: 1,
sum: 1,
dur: 0,
segmentation: {}
}

interface eventStore {
eventData: CountlyEventData
startTime: number
timeout: NodeJS.Timeout
}

/**
* EventAccumulator is a class that accumulates events and flushes them to the Countly server.
*/
export class EventAccumulator<T extends CountlyWebSdk | CountlyNodeSdk> implements IEventAccumulator {
private readonly metricsService: T
private readonly events: Map<string, eventStore> = new Map()
private readonly flushInterval: number

/**
* Create a new EventAccumulator
*
* @param {CountlyWebSdk} metricsService - instance
* @param {number} flushInterval - in milliseconds
*/
constructor (metricsService: T, flushInterval: number = 5 * 60 * 1000) {
this.metricsService = metricsService
this.flushInterval = flushInterval
this.setupUnloadEvent()
}

/**
* Setup the beforeunload event to flush all events.
*/
private setupUnloadEvent (): void {
const flushAllHandler = (): void => {
this.flushAll()
}

if (typeof globalThis.addEventListener === 'function') {
globalThis.addEventListener('beforeunload', flushAllHandler)
} else if (typeof globalThis.process?.on === 'function') {
globalThis.process.on('beforeExit', flushAllHandler)
}
}

/**
* Pad events with default values
*
* @param {CountlyEvent} event - event to add defaults to
* @returns {CountlyEventData} - event with defaults added
*/
private addEventDefaults (event: CountlyEvent): CountlyEventData {
return { ...eventDefaults, ...event }
}

/**
* Setup the event accumulator for a key type for the first time.
*
* @param {CountlyEventData} eventData - event data
*/
private setupEventAccumulator (eventData: CountlyEventData): void {
const { key } = eventData
this.events.set(key, {
eventData,
// set start time to now. This will be updated when the event is flushed.
startTime: Date.now(),
// set a timeout to flush the event after the flush interval.
timeout: setTimeout(() => {
this.flush(key)
}, this.flushInterval)
})
}

/**
* Digest only the event data from an event.
*
* @param {CountlyEventData} newEventData - event data
*/
private accumulateEventData (newEventData: CountlyEventData): void {
const { key, count, segmentation } = newEventData
// if event is in the store, update the event data.
const eventStore = this.events.get(key)
if (eventStore == null) {
this.setupEventAccumulator(newEventData)
return
}
const { eventData } = eventStore
eventData.count += count
eventData.sum += 1
eventData.segmentation = { ...eventData.segmentation, ...segmentation }
}

/**
* Add an event to the accumulator
*
* @param {CountlyEvent} event - event to add
* @param {boolean} flush - optionally whether to flush the event immediately
*/
addEvent (event: CountlyEvent, flush: boolean = false): void {
const eventData = this.addEventDefaults(event)
const { key } = eventData

// validate event
if (key === '') {
throw new Error('Event key is required.')
}

this.accumulateEventData(eventData)

// flush the event if flush is true.
if (flush) {
this.flush(key)
}
}

/**
* Flush an event from the accumulator
*
* @param {string} key - event key
*/
flush (key: string): void {
const eventStore = this.events.get(key)
if (eventStore == null) {
return
}

const { eventData, startTime, timeout } = eventStore

// update duration to ms from start.
eventData.dur = Date.now() - startTime
// add event to the async queue.
this.metricsService.add_event(eventData)
clearTimeout(timeout)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
this.events.delete(key)
}

/**
* Flush all events from the accumulator
*/
flushAll (): void {
this.events.forEach((_, key) => {
this.flush(key)
})
}
}
21 changes: 15 additions & 6 deletions src/MetricsProvider.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import type {
CountlyEvent,
CountlyWebSdk,
IgnoreList,
Segments,
metricFeatures
} from 'countly-sdk-web'
import type { consentTypes, consentTypesExceptAll } from '../types/index.js'
import { COUNTLY_SETUP_DEFAULTS } from './config.js'

import type { metricFeatures, CountlyWebSdk, IgnoreList, Segments, CountlyEvent } from 'countly-sdk-web'
import type { CountlyNodeSdk } from 'countly-sdk-nodejs'
import type { consentTypes, consentTypesExceptAll } from './types/index.js'
import { EventAccumulator } from './EventAccumulator.js'
import type { StorageProvider } from './StorageProvider.js'

export interface MetricsProviderConstructorOptions<T> {
appKey: string
autoTrack?: boolean
interval?: number
max_events?: number
metricsService: T
queue_size?: number
session_update?: number
url?: string
metricsService: T
storageProvider?: StorageProvider | null
}

export default class MetricsProvider<T extends CountlyWebSdk | CountlyNodeSdk> {
public readonly accumulate: EventAccumulator<T>
private readonly groupedFeatures: Record<consentTypes, metricFeatures[]> = this.mapAllEvents({
minimal: ['sessions', 'views', 'events'],
performance: ['crashes', 'apm'],
Expand All @@ -33,16 +41,17 @@ export default class MetricsProvider<T extends CountlyWebSdk | CountlyNodeSdk> {
private readonly initDone: boolean = false

constructor (config: MetricsProviderConstructorOptions<T>) {
const { appKey, ...remainderConfig } = config
const serviceConfig = {
...COUNTLY_SETUP_DEFAULTS,
...config,
app_key: config.appKey
...remainderConfig,
app_key: appKey
}
const { autoTrack, metricsService, storageProvider } = serviceConfig
this.metricsService = metricsService
this.storageProvider = storageProvider ?? null

this.metricsService.init(serviceConfig)
this.accumulate = new EventAccumulator(metricsService)
this.metricsService.group_features(this.groupedFeatures)

if (autoTrack) {
Expand Down
5 changes: 2 additions & 3 deletions src/NodeMetricsProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Countly from 'countly-sdk-nodejs'

import type { MetricProviderOptionalConstructorArgs, WithOptional } from '../types/index.js'
import MetricsProvider, { MetricsProviderConstructorOptions } from './MetricsProvider.js'
import type { MetricProviderOptionalConstructorArgs, WithOptional } from './types/index.js'
import Countly from 'countly-sdk-nodejs'

export class NodeMetricsProvider extends MetricsProvider<typeof Countly> {
constructor (args: WithOptional<MetricsProviderConstructorOptions<typeof Countly>, MetricProviderOptionalConstructorArgs>) {
Expand Down
2 changes: 1 addition & 1 deletion src/StorageProvider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { consentTypes } from './types/index.js'
import type { consentTypes } from '../types/index.js'

export interface StorageProviderInterface {
setStore: (values: consentTypes[]) => void
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ export * from './config.js'
export * from './BrowserMetricsProvider.js'
export * from './MetricsProvider.js'
export * from './NodeMetricsProvider.js'
export * from './types/index.js'
export * from '../types/index.js'
1 change: 0 additions & 1 deletion test/node.spec.ts

This file was deleted.

Loading

0 comments on commit 2d43252

Please sign in to comment.