Files
stats/packages/sdk/index.ts
2023-10-12 13:51:11 +02:00

236 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { v4 as uuid } from 'uuid'
import {
EventPayload,
MixanErrorResponse,
MixanResponse,
ProfilePayload,
} from '@mixan/types'
type MixanOptions = {
url: string
clientId: string
clientSecret: string
batchInterval?: number
maxBatchSize?: number
verbose?: boolean
saveProfileId: (profileId: string) => void,
getProfileId: () => string | null,
removeProfileId: () => void,
}
class Fetcher {
private url: string
private clientId: string
private clientSecret: string
private logger: (...args: any[]) => void
constructor(options: MixanOptions) {
this.url = options.url
this.clientId = options.clientId
this.clientSecret = options.clientSecret
this.logger = options.verbose ? console.log : () => {}
}
post(
path: string,
data: Record<string, any>,
options: FetchRequestInit = {}
) {
const url = `${this.url}${path}`
this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2))
return fetch(url, {
headers: {
['mixan-client-id']: this.clientId,
['mixan-client-secret']: this.clientSecret,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(data),
...options,
})
.then(async (res) => {
const response = await res.json<
MixanErrorResponse | MixanResponse<unknown>
>()
if('status' in response && response.status === 'error') {
this.logger(`Mixan request failed: ${url}`, JSON.stringify(response, null, 2))
return null
}
return response
})
.catch(() => {
return null
})
}
}
class Batcher<T extends any> {
queue: T[] = []
timer?: Timer
callback: (queue: T[]) => void
maxBatchSize = 10
batchInterval = 10000
constructor(options: MixanOptions, callback: (queue: T[]) => void) {
this.callback = callback
if (options.maxBatchSize) {
this.maxBatchSize = options.maxBatchSize
}
if (options.batchInterval) {
this.batchInterval = options.batchInterval
}
}
add(payload: T) {
this.queue.push(payload)
this.flush()
}
flush() {
if (this.timer) {
clearTimeout(this.timer)
}
if (this.queue.length === 0) {
return
}
if (this.queue.length >= this.maxBatchSize) {
this.send()
return
}
this.timer = setTimeout(this.send.bind(this), this.batchInterval)
}
send() {
this.callback(this.queue)
this.queue = []
}
}
export class Mixan {
private fetch: Fetcher
private eventBatcher: Batcher<EventPayload>
private profileId?: string
private options: MixanOptions
private logger: (...args: any[]) => void
constructor(options: MixanOptions) {
this.logger = options.verbose ? console.log : () => {}
this.options = options
this.fetch = new Fetcher(options)
this.setAnonymousUser()
this.eventBatcher = new Batcher(options, (queue) => {
this.fetch.post(
'/events',
queue.map((item) => ({
...item,
profileId: item.profileId || this.profileId || null,
}))
)
})
}
timestamp() {
return new Date().toISOString()
}
event(name: string, properties: Record<string, any>) {
this.logger('Mixan: Queue event', name)
this.eventBatcher.add({
name,
properties,
time: this.timestamp(),
profileId: this.profileId || null,
})
}
private setAnonymousUser() {
const profileId = this.options.getProfileId()
if(profileId) {
this.profileId = profileId
this.logger('Mixan: Use existing ID', this.profileId);
} else {
this.profileId = uuid()
this.logger('Mixan: Create new ID', this.profileId);
this.options.saveProfileId(this.profileId)
this.fetch.post('/profiles', {
id: this.profileId,
properties: {},
})
}
}
async setUser(profile: ProfilePayload) {
if(!this.profileId) {
return this.logger('Mixan: Set user failed, no profileId');
}
this.logger('Mixan: Set user', profile);
await this.fetch.post(`/profiles/${this.profileId}`, profile, {
method: 'PUT'
})
}
async setUserProperty(name: string, value: any) {
if(!this.profileId) {
return this.logger('Mixan: Set user property, no profileId');
}
this.logger('Mixan: Set user property', name, value);
await this.fetch.post(`/profiles/${this.profileId}`, {
properties: {
[name]: value,
},
})
}
async increment(name: string, value: number = 1) {
if (!this.profileId) {
this.logger('Mixan: Increment failed, no profileId');
return
}
this.logger('Mixan: Increment user property', name, value);
await this.fetch.post(`/profiles/${this.profileId}/increment`, {
name,
value,
}, {
method: 'PUT'
})
}
async decrement(name: string, value: number = 1) {
if (!this.profileId) {
this.logger('Mixan: Decrement failed, no profileId');
return
}
this.logger('Mixan: Decrement user property', name, value);
await this.fetch.post(`/profiles/${this.profileId}/decrement`, {
name,
value,
}, {
method: 'PUT'
})
}
async screenView(route: string, properties?: Record<string, any>) {
await this.event('screen_view', {
...(properties || {}),
route,
})
}
clear() {
this.logger('Mixan: Clear, send remaining events and remove profileId');
this.eventBatcher.send()
this.options.removeProfileId()
this.profileId = undefined
this.setAnonymousUser()
}
}