sdk: remove uuid dependency and get profile id from api

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-11-02 09:25:38 +01:00
parent bbbc16be25
commit 4994f7af76
2 changed files with 124 additions and 78 deletions

View File

@@ -1,22 +1,22 @@
import { v4 as uuid } from 'uuid'
import { import {
EventPayload, EventPayload,
MixanErrorResponse, MixanErrorResponse,
MixanResponse,
ProfilePayload, ProfilePayload,
} from '@mixan/types' } from '@mixan/types'
type MixanOptions = { type NewMixanOptions = {
url: string url: string
clientId: string clientId: string
clientSecret: string clientSecret: string
batchInterval?: number batchInterval?: number
maxBatchSize?: number maxBatchSize?: number
sessionTimeout?: number
verbose?: boolean verbose?: boolean
saveProfileId: (profileId: string) => void, saveProfileId: (profiId: string) => void
getProfileId: () => string | null, getProfileId: () => (string | null)
removeProfileId: () => void, removeProfileId: () => void
} }
type MixanOptions = Required<NewMixanOptions>
class Fetcher { class Fetcher {
private url: string private url: string
@@ -31,11 +31,11 @@ class Fetcher {
this.logger = options.verbose ? console.log : () => {} this.logger = options.verbose ? console.log : () => {}
} }
post( post<Response extends unknown>(
path: string, path: string,
data: Record<string, any>, data: Record<string, any> = {},
options: FetchRequestInit = {} options: FetchRequestInit = {}
) { ): Promise<Response | null> {
const url = `${this.url}${path}` const url = `${this.url}${path}`
this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2)) this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2))
return fetch(url, { return fetch(url, {
@@ -50,17 +50,27 @@ class Fetcher {
}) })
.then(async (res) => { .then(async (res) => {
const response = await res.json< const response = await res.json<
MixanErrorResponse | MixanResponse<unknown> MixanErrorResponse | Response
>() >()
if('status' in response && response.status === 'error') { if(!response) {
this.logger(`Mixan request failed: [${options.method || 'POST'}] ${url}`, JSON.stringify(response, null, 2))
return null return null
} }
return response if (typeof response === 'object' && 'status' in response && response.status === 'error') {
this.logger(
`Mixan request failed: [${options.method || 'POST'}] ${url}`,
JSON.stringify(response, null, 2)
)
return null
}
return response as Response
}) })
.catch(() => { .catch(() => {
this.logger(
`Mixan request failed: [${options.method || 'POST'}] ${url}`
)
return null return null
}) })
} }
@@ -70,19 +80,13 @@ class Batcher<T extends any> {
queue: T[] = [] queue: T[] = []
timer?: Timer timer?: Timer
callback: (queue: T[]) => void callback: (queue: T[]) => void
maxBatchSize = 10 maxBatchSize: number
batchInterval = 10000 batchInterval: number
constructor(options: MixanOptions, callback: (queue: T[]) => void) { constructor(options: MixanOptions, callback: (queue: T[]) => void) {
this.callback = callback this.callback = callback
this.maxBatchSize = options.maxBatchSize
if (options.maxBatchSize) { this.batchInterval = options.batchInterval
this.maxBatchSize = options.maxBatchSize
}
if (options.batchInterval) {
this.batchInterval = options.batchInterval
}
} }
add(payload: T) { add(payload: T) {
@@ -108,11 +112,11 @@ class Batcher<T extends any> {
} }
send() { send() {
if(this.timer) { if (this.timer) {
clearTimeout(this.timer) clearTimeout(this.timer)
} }
if(this.queue.length > 0) { if (this.queue.length > 0) {
this.callback(this.queue) this.callback(this.queue)
this.queue = [] this.queue = []
} }
@@ -126,18 +130,29 @@ export class Mixan {
private options: MixanOptions private options: MixanOptions
private logger: (...args: any[]) => void private logger: (...args: any[]) => void
private globalProperties: Record<string, any> = {} private globalProperties: Record<string, any> = {}
private lastEventAt?: string
private lastScreenViewAt?: string private lastScreenViewAt?: string
constructor(options: MixanOptions) { constructor(options: NewMixanOptions) {
this.logger = options.verbose ? console.log : () => {} this.logger = options.verbose ? console.log : () => {}
this.options = options this.options = {
this.fetch = new Fetcher(options) sessionTimeout: 1000 * 60 * 30,
this.setAnonymousUser() verbose: false,
this.eventBatcher = new Batcher(options, (queue) => { batchInterval: 10000,
maxBatchSize: 10,
...options,
}
this.fetch = new Fetcher(this.options)
this.eventBatcher = new Batcher(this.options, (queue) => {
this.fetch.post( this.fetch.post(
'/events', '/events',
queue.map((item) => ({ queue.map((item) => ({
...item, ...item,
properties: {
...this.globalProperties,
...item.properties,
},
profileId: item.profileId || this.profileId || null, profileId: item.profileId || this.profileId || null,
})) }))
) )
@@ -148,50 +163,74 @@ export class Mixan {
return new Date().toISOString() return new Date().toISOString()
} }
event(name: string, properties: Record<string, any>) { init() {
this.logger('Mixan: Init')
this.setAnonymousUser()
}
event(name: string, properties: Record<string, any> = {}) {
const now = new Date()
const isSessionStart =
now.getTime() - new Date(this.lastEventAt ?? '1970-01-01').getTime() >
this.options.sessionTimeout
if (isSessionStart) {
this.logger('Mixan: Session start')
this.eventBatcher.add({
name: 'session_start',
time: this.timestamp(),
properties: {},
profileId: this.profileId || null,
})
}
this.logger('Mixan: Queue event', name) this.logger('Mixan: Queue event', name)
this.eventBatcher.add({ this.eventBatcher.add({
name, name,
properties: { properties,
...this.globalProperties,
...properties,
},
time: this.timestamp(), time: this.timestamp(),
profileId: this.profileId || null, profileId: this.profileId || null,
}) })
this.lastEventAt = this.timestamp()
} }
private setAnonymousUser() { private async setAnonymousUser(retryCount: number = 0) {
const profileId = this.options.getProfileId() const profileId = this.options.getProfileId()
if(profileId) { if (profileId) {
this.profileId = profileId this.profileId = profileId
this.logger('Mixan: Use existing ID', this.profileId); this.logger('Mixan: Use existing profile', this.profileId)
} else { } else {
this.profileId = uuid() const res = await this.fetch.post<{id: string}>('/profiles')
this.logger('Mixan: Create new ID', this.profileId);
this.options.saveProfileId(this.profileId) if(res) {
this.fetch.post('/profiles', { this.profileId = res.id
id: this.profileId, this.options.saveProfileId(res.id)
properties: {}, this.logger('Mixan: Create new profile', this.profileId)
}) } else if(retryCount < 2) {
setTimeout(() => {
this.setAnonymousUser(retryCount + 1)
}, 500);
} else {
this.logger('Mixan: Failed to create new profile')
}
} }
} }
async setUser(profile: ProfilePayload) { async setUser(profile: ProfilePayload) {
if(!this.profileId) { if (!this.profileId) {
return this.logger('Mixan: Set user failed, no profileId'); return this.logger('Mixan: Set user failed, no profileId')
} }
this.logger('Mixan: Set user', profile); this.logger('Mixan: Set user', profile)
await this.fetch.post(`/profiles/${this.profileId}`, profile, { await this.fetch.post(`/profiles/${this.profileId}`, profile, {
method: 'PUT' method: 'PUT',
}) })
} }
async setUserProperty(name: string, value: any) { async setUserProperty(name: string, value: any) {
if(!this.profileId) { if (!this.profileId) {
return this.logger('Mixan: Set user property, no profileId'); return this.logger('Mixan: Set user property, no profileId')
} }
this.logger('Mixan: Set user property', name, value); this.logger('Mixan: Set user property', name, value)
await this.fetch.post(`/profiles/${this.profileId}`, { await this.fetch.post(`/profiles/${this.profileId}`, {
properties: { properties: {
[name]: value, [name]: value,
@@ -200,45 +239,53 @@ export class Mixan {
} }
async setGlobalProperties(properties: Record<string, any>) { async setGlobalProperties(properties: Record<string, any>) {
this.logger('Mixan: Set global properties', properties); this.logger('Mixan: Set global properties', properties)
this.globalProperties = properties ?? {} this.globalProperties = properties ?? {}
} }
async increment(name: string, value: number = 1) { async increment(name: string, value: number = 1) {
if (!this.profileId) { if (!this.profileId) {
this.logger('Mixan: Increment failed, no profileId'); this.logger('Mixan: Increment failed, no profileId')
return return
} }
this.logger('Mixan: Increment user property', name, value); this.logger('Mixan: Increment user property', name, value)
await this.fetch.post(`/profiles/${this.profileId}/increment`, { await this.fetch.post(
name, `/profiles/${this.profileId}/increment`,
value, {
}, { name,
method: 'PUT' value,
}) },
{
method: 'PUT',
}
)
} }
async decrement(name: string, value: number = 1) { async decrement(name: string, value: number = 1) {
if (!this.profileId) { if (!this.profileId) {
this.logger('Mixan: Decrement failed, no profileId'); this.logger('Mixan: Decrement failed, no profileId')
return return
} }
this.logger('Mixan: Decrement user property', name, value); this.logger('Mixan: Decrement user property', name, value)
await this.fetch.post(`/profiles/${this.profileId}/decrement`, { await this.fetch.post(
name, `/profiles/${this.profileId}/decrement`,
value, {
}, { name,
method: 'PUT' value,
}) },
{
method: 'PUT',
}
)
} }
async screenView(route: string, _properties?: Record<string, any>) { async screenView(route: string, _properties?: Record<string, any>) {
const properties = _properties ?? {} const properties = _properties ?? {}
const now = new Date() const now = new Date()
if(this.lastScreenViewAt) { if (this.lastScreenViewAt) {
const last = new Date(this.lastScreenViewAt) const last = new Date(this.lastScreenViewAt)
const diff = now.getTime() - last.getTime() const diff = now.getTime() - last.getTime()
this.logger(`Mixan: Screen view duration: ${diff}ms`) this.logger(`Mixan: Screen view duration: ${diff}ms`)
@@ -259,7 +306,7 @@ export class Mixan {
} }
clear() { clear() {
this.logger('Mixan: Clear, send remaining events and remove profileId'); this.logger('Mixan: Clear, send remaining events and remove profileId')
this.eventBatcher.send() this.eventBatcher.send()
this.options.removeProfileId() this.options.removeProfileId()
this.profileId = undefined this.profileId = undefined

View File

@@ -4,8 +4,7 @@
"type": "module", "type": "module",
"module": "index.ts", "module": "index.ts",
"dependencies": { "dependencies": {
"@mixan/types": "workspace:*", "@mixan/types": "workspace:*"
"uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/uuid": "^9.0.5", "@types/uuid": "^9.0.5",