added tooling (eslint, typescript and prettier)
This commit is contained in:
@@ -1,43 +1,43 @@
|
||||
import {
|
||||
import type {
|
||||
EventPayload,
|
||||
MixanErrorResponse,
|
||||
ProfilePayload,
|
||||
} from '@mixan/types'
|
||||
} from '@mixan/types';
|
||||
|
||||
type NewMixanOptions = {
|
||||
url: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
batchInterval?: number
|
||||
maxBatchSize?: number
|
||||
sessionTimeout?: number
|
||||
verbose?: boolean
|
||||
saveProfileId: (profiId: string) => void
|
||||
getProfileId: () => (string | null)
|
||||
removeProfileId: () => void
|
||||
interface NewMixanOptions {
|
||||
url: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
batchInterval?: number;
|
||||
maxBatchSize?: number;
|
||||
sessionTimeout?: number;
|
||||
verbose?: boolean;
|
||||
saveProfileId: (profiId: string) => void;
|
||||
getProfileId: () => string | null;
|
||||
removeProfileId: () => void;
|
||||
}
|
||||
type MixanOptions = Required<NewMixanOptions>
|
||||
type MixanOptions = Required<NewMixanOptions>;
|
||||
|
||||
class Fetcher {
|
||||
private url: string
|
||||
private clientId: string
|
||||
private clientSecret: string
|
||||
private logger: (...args: any[]) => void
|
||||
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 : () => {}
|
||||
this.url = options.url;
|
||||
this.clientId = options.clientId;
|
||||
this.clientSecret = options.clientSecret;
|
||||
this.logger = options.verbose ? console.log : () => {};
|
||||
}
|
||||
|
||||
post<Response extends unknown>(
|
||||
post<PostData, PostResponse>(
|
||||
path: string,
|
||||
data: Record<string, any> = {},
|
||||
options: RequestInit = {}
|
||||
): Promise<Response | null> {
|
||||
const url = `${this.url}${path}`
|
||||
this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2))
|
||||
data?: PostData,
|
||||
options?: RequestInit
|
||||
): Promise<PostResponse | null> {
|
||||
const url = `${this.url}${path}`;
|
||||
this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2));
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
['mixan-client-id']: this.clientId,
|
||||
@@ -45,103 +45,109 @@ class Fetcher {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
...options,
|
||||
body: JSON.stringify(data ?? {}),
|
||||
...(options ?? {}),
|
||||
})
|
||||
.then(async (res) => {
|
||||
const response = await res.json() as (MixanErrorResponse | Response)
|
||||
const response = (await res.json()) as
|
||||
| MixanErrorResponse
|
||||
| PostResponse;
|
||||
|
||||
if(!response) {
|
||||
return null
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof response === 'object' && 'status' in response && response.status === 'error') {
|
||||
if (
|
||||
typeof response === 'object' &&
|
||||
'status' in response &&
|
||||
response.status === 'error'
|
||||
) {
|
||||
this.logger(
|
||||
`Mixan request failed: [${options.method || 'POST'}] ${url}`,
|
||||
`Mixan request failed: [${options?.method ?? 'POST'}] ${url}`,
|
||||
JSON.stringify(response, null, 2)
|
||||
)
|
||||
return null
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return response as Response
|
||||
return response as PostResponse;
|
||||
})
|
||||
.catch(() => {
|
||||
this.logger(
|
||||
`Mixan request failed: [${options.method || 'POST'}] ${url}`
|
||||
)
|
||||
return null
|
||||
})
|
||||
`Mixan request failed: [${options?.method ?? 'POST'}] ${url}`
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Batcher<T extends any> {
|
||||
queue: T[] = []
|
||||
timer?: NodeJS.Timeout
|
||||
callback: (queue: T[]) => void
|
||||
maxBatchSize: number
|
||||
batchInterval: number
|
||||
class Batcher<T> {
|
||||
queue: T[] = [];
|
||||
timer?: NodeJS.Timeout;
|
||||
callback: (queue: T[]) => void;
|
||||
maxBatchSize: number;
|
||||
batchInterval: number;
|
||||
|
||||
constructor(options: MixanOptions, callback: (queue: T[]) => void) {
|
||||
this.callback = callback
|
||||
this.maxBatchSize = options.maxBatchSize
|
||||
this.batchInterval = options.batchInterval
|
||||
this.callback = callback;
|
||||
this.maxBatchSize = options.maxBatchSize;
|
||||
this.batchInterval = options.batchInterval;
|
||||
}
|
||||
|
||||
add(payload: T) {
|
||||
this.queue.push(payload)
|
||||
this.flush()
|
||||
this.queue.push(payload);
|
||||
this.flush();
|
||||
}
|
||||
|
||||
flush() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
if (this.queue.length === 0) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.queue.length >= this.maxBatchSize) {
|
||||
this.send()
|
||||
return
|
||||
this.send();
|
||||
return;
|
||||
}
|
||||
|
||||
this.timer = setTimeout(this.send.bind(this), this.batchInterval)
|
||||
this.timer = setTimeout(this.send.bind(this), this.batchInterval);
|
||||
}
|
||||
|
||||
send() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
if (this.queue.length > 0) {
|
||||
this.callback(this.queue)
|
||||
this.queue = []
|
||||
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
|
||||
private globalProperties: Record<string, any> = {}
|
||||
private lastEventAt?: string
|
||||
private lastScreenViewAt?: string
|
||||
private fetch: Fetcher;
|
||||
private eventBatcher: Batcher<EventPayload>;
|
||||
private profileId?: string;
|
||||
private options: MixanOptions;
|
||||
private logger: (...args: any[]) => void;
|
||||
private globalProperties: Record<string, unknown> = {};
|
||||
private lastEventAt?: string;
|
||||
private lastScreenViewAt?: string;
|
||||
|
||||
constructor(options: NewMixanOptions) {
|
||||
this.logger = options.verbose ? console.log : () => {}
|
||||
this.logger = options.verbose ? console.log : () => {};
|
||||
this.options = {
|
||||
sessionTimeout: 1000 * 60 * 30,
|
||||
verbose: false,
|
||||
batchInterval: 10000,
|
||||
maxBatchSize: 10,
|
||||
...options,
|
||||
}
|
||||
};
|
||||
|
||||
this.fetch = new Fetcher(this.options)
|
||||
this.fetch = new Fetcher(this.options);
|
||||
this.eventBatcher = new Batcher(this.options, (queue) => {
|
||||
this.fetch.post(
|
||||
'/events',
|
||||
@@ -151,103 +157,106 @@ export class Mixan {
|
||||
...this.globalProperties,
|
||||
...item.properties,
|
||||
},
|
||||
profileId: item.profileId || this.profileId || null,
|
||||
profileId: item.profileId ?? this.profileId ?? null,
|
||||
}))
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
timestamp() {
|
||||
return new Date().toISOString()
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.logger('Mixan: Init')
|
||||
this.setAnonymousUser()
|
||||
this.logger('Mixan: Init');
|
||||
this.setAnonymousUser();
|
||||
}
|
||||
|
||||
event(name: string, properties: Record<string, any> = {}) {
|
||||
const now = new Date()
|
||||
event(name: string, properties: Record<string, unknown> = {}) {
|
||||
const now = new Date();
|
||||
const isSessionStart =
|
||||
now.getTime() - new Date(this.lastEventAt ?? '1970-01-01').getTime() >
|
||||
this.options.sessionTimeout
|
||||
this.options.sessionTimeout;
|
||||
|
||||
if (isSessionStart) {
|
||||
this.logger('Mixan: Session start')
|
||||
this.logger('Mixan: Session start');
|
||||
this.eventBatcher.add({
|
||||
name: 'session_start',
|
||||
time: this.timestamp(),
|
||||
properties: {},
|
||||
profileId: this.profileId || null,
|
||||
})
|
||||
profileId: this.profileId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger('Mixan: Queue event', name)
|
||||
this.logger('Mixan: Queue event', name);
|
||||
this.eventBatcher.add({
|
||||
name,
|
||||
properties,
|
||||
time: this.timestamp(),
|
||||
profileId: this.profileId || null,
|
||||
})
|
||||
this.lastEventAt = this.timestamp()
|
||||
profileId: this.profileId ?? null,
|
||||
});
|
||||
this.lastEventAt = this.timestamp();
|
||||
}
|
||||
|
||||
private async setAnonymousUser(retryCount: number = 0) {
|
||||
const profileId = this.options.getProfileId()
|
||||
private async setAnonymousUser(retryCount = 0) {
|
||||
const profileId = this.options.getProfileId();
|
||||
if (profileId) {
|
||||
this.profileId = profileId
|
||||
this.logger('Mixan: Use existing profile', this.profileId)
|
||||
this.profileId = profileId;
|
||||
this.logger('Mixan: Use existing profile', this.profileId);
|
||||
} else {
|
||||
const res = await this.fetch.post<{id: string}>('/profiles')
|
||||
|
||||
if(res) {
|
||||
this.profileId = res.id
|
||||
this.options.saveProfileId(res.id)
|
||||
this.logger('Mixan: Create new profile', this.profileId)
|
||||
} else if(retryCount < 2) {
|
||||
const res = await this.fetch.post<undefined, { id: string }>('/profiles');
|
||||
|
||||
if (res) {
|
||||
this.profileId = res.id;
|
||||
this.options.saveProfileId(res.id);
|
||||
this.logger('Mixan: Create new profile', this.profileId);
|
||||
} else if (retryCount < 2) {
|
||||
setTimeout(() => {
|
||||
this.setAnonymousUser(retryCount + 1)
|
||||
this.setAnonymousUser(retryCount + 1);
|
||||
}, 500);
|
||||
} else {
|
||||
this.logger('Mixan: Failed to create new profile')
|
||||
this.logger('Mixan: Failed to create new profile');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setUser(profile: ProfilePayload) {
|
||||
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, {
|
||||
method: 'PUT',
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async setUserProperty(name: string, value: any) {
|
||||
async setUserProperty(
|
||||
name: string,
|
||||
value: string | number | boolean | Record<string, unknown> | unknown[]
|
||||
) {
|
||||
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}`, {
|
||||
properties: {
|
||||
[name]: value,
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async setGlobalProperties(properties: Record<string, any>) {
|
||||
this.logger('Mixan: Set global properties', properties)
|
||||
this.globalProperties = properties ?? {}
|
||||
setGlobalProperties(properties: Record<string, unknown>) {
|
||||
this.logger('Mixan: Set global properties', properties);
|
||||
this.globalProperties = properties ?? {};
|
||||
}
|
||||
|
||||
async increment(name: string, value: number = 1) {
|
||||
async increment(name: string, value = 1) {
|
||||
if (!this.profileId) {
|
||||
this.logger('Mixan: Increment failed, no profileId')
|
||||
return
|
||||
this.logger('Mixan: Increment failed, no profileId');
|
||||
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`,
|
||||
{
|
||||
@@ -257,16 +266,16 @@ export class Mixan {
|
||||
{
|
||||
method: 'PUT',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async decrement(name: string, value: number = 1) {
|
||||
async decrement(name: string, value = 1) {
|
||||
if (!this.profileId) {
|
||||
this.logger('Mixan: Decrement failed, no profileId')
|
||||
return
|
||||
this.logger('Mixan: Decrement failed, no profileId');
|
||||
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`,
|
||||
{
|
||||
@@ -276,38 +285,38 @@ export class Mixan {
|
||||
{
|
||||
method: 'PUT',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async screenView(route: string, _properties?: Record<string, any>) {
|
||||
const properties = _properties ?? {}
|
||||
const now = new Date()
|
||||
screenView(route: string, _properties?: Record<string, unknown>) {
|
||||
const properties = _properties ?? {};
|
||||
const now = new Date();
|
||||
|
||||
if (this.lastScreenViewAt) {
|
||||
const last = new Date(this.lastScreenViewAt)
|
||||
const diff = now.getTime() - last.getTime()
|
||||
this.logger(`Mixan: Screen view duration: ${diff}ms`)
|
||||
properties['duration'] = diff
|
||||
const last = new Date(this.lastScreenViewAt);
|
||||
const diff = now.getTime() - last.getTime();
|
||||
this.logger(`Mixan: Screen view duration: ${diff}ms`);
|
||||
properties.duration = diff;
|
||||
}
|
||||
|
||||
this.lastScreenViewAt = now.toISOString()
|
||||
await this.event('screen_view', {
|
||||
this.lastScreenViewAt = now.toISOString();
|
||||
this.event('screen_view', {
|
||||
...properties,
|
||||
route,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
flush() {
|
||||
this.logger('Mixan: Flushing events queue')
|
||||
this.eventBatcher.send()
|
||||
this.lastScreenViewAt = undefined
|
||||
this.logger('Mixan: Flushing events queue');
|
||||
this.eventBatcher.send();
|
||||
this.lastScreenViewAt = undefined;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.logger('Mixan: Clear, send remaining events and remove profileId')
|
||||
this.eventBatcher.send()
|
||||
this.options.removeProfileId()
|
||||
this.profileId = undefined
|
||||
this.setAnonymousUser()
|
||||
this.logger('Mixan: Clear, send remaining events and remove profileId');
|
||||
this.eventBatcher.send();
|
||||
this.options.removeProfileId();
|
||||
this.profileId = undefined;
|
||||
this.setAnonymousUser();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,25 @@
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mixan/types": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.5",
|
||||
"@mixan/eslint-config": "workspace:*",
|
||||
"@mixan/prettier-config": "workspace:*",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
"typescript": "^5.2.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@mixan/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@mixan/prettier-config"
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
|
||||
|
||||
"outDir": "dist",
|
||||
"allowImportingTsExtensions": false,
|
||||
"noEmit": false,
|
||||
"noEmit": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from "tsup";
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["index.ts"],
|
||||
format: ["cjs", "esm"], // Build for commonJS and ESmodules
|
||||
entry: ['index.ts'],
|
||||
format: ['cjs', 'esm'], // Build for commonJS and ESmodules
|
||||
dts: true, // Generate declaration file (.d.ts)
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
export type MixanJson = Record<string, any>
|
||||
export type MixanJson = Record<string, any>;
|
||||
|
||||
export type EventPayload = {
|
||||
name: string
|
||||
time: string
|
||||
profileId: string | null
|
||||
properties: MixanJson
|
||||
export interface EventPayload {
|
||||
name: string;
|
||||
time: string;
|
||||
profileId: string | null;
|
||||
properties: MixanJson;
|
||||
}
|
||||
|
||||
export type ProfilePayload = {
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
email?: string
|
||||
avatar?: string
|
||||
id?: string
|
||||
properties?: MixanJson
|
||||
export interface ProfilePayload {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
id?: string;
|
||||
properties?: MixanJson;
|
||||
}
|
||||
|
||||
export type ProfileIncrementPayload = {
|
||||
name: string
|
||||
value: number
|
||||
id: string
|
||||
export interface ProfileIncrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type ProfileDecrementPayload = {
|
||||
name: string
|
||||
value: number
|
||||
id: string
|
||||
export interface ProfileDecrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
// Batching
|
||||
export type BatchEvent = {
|
||||
type: 'event'
|
||||
payload: EventPayload
|
||||
export interface BatchEvent {
|
||||
type: 'event';
|
||||
payload: EventPayload;
|
||||
}
|
||||
|
||||
export type BatchProfile = {
|
||||
type: 'profile'
|
||||
payload: ProfilePayload
|
||||
export interface BatchProfile {
|
||||
type: 'profile';
|
||||
payload: ProfilePayload;
|
||||
}
|
||||
|
||||
export type BatchProfileIncrement = {
|
||||
type: 'profile_increment'
|
||||
payload: ProfileIncrementPayload
|
||||
export interface BatchProfileIncrement {
|
||||
type: 'profile_increment';
|
||||
payload: ProfileIncrementPayload;
|
||||
}
|
||||
|
||||
export type BatchProfileDecrement = {
|
||||
type: 'profile_decrement'
|
||||
payload: ProfileDecrementPayload
|
||||
export interface BatchProfileDecrement {
|
||||
type: 'profile_decrement';
|
||||
payload: ProfileDecrementPayload;
|
||||
}
|
||||
|
||||
export type BatchItem =
|
||||
| BatchEvent
|
||||
| BatchProfile
|
||||
| BatchProfileIncrement
|
||||
| BatchProfileDecrement
|
||||
export type BatchPayload = Array<BatchItem>
|
||||
| BatchProfileDecrement;
|
||||
export type BatchPayload = BatchItem[];
|
||||
|
||||
export type MixanIssue = {
|
||||
field: string
|
||||
message: string
|
||||
value: any
|
||||
export interface MixanIssue {
|
||||
field: string;
|
||||
message: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export type MixanErrorResponse = {
|
||||
status: 'error'
|
||||
code: number
|
||||
message: string
|
||||
issues?: Array<MixanIssue> | undefined
|
||||
stack?: string | undefined
|
||||
export interface MixanErrorResponse {
|
||||
status: 'error';
|
||||
code: number;
|
||||
message: string;
|
||||
issues?: MixanIssue[] | undefined;
|
||||
stack?: string | undefined;
|
||||
}
|
||||
|
||||
export type MixanResponse<T> = {
|
||||
result: T
|
||||
status: 'ok'
|
||||
export interface MixanResponse<T> {
|
||||
result: T;
|
||||
status: 'ok';
|
||||
}
|
||||
|
||||
@@ -3,8 +3,22 @@
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mixan/eslint-config": "workspace:*",
|
||||
"@mixan/prettier-config": "workspace:*",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
"typescript": "^5.2.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@mixan/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@mixan/prettier-config"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from "tsup";
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["index.ts"],
|
||||
format: ["cjs", "esm"], // Build for commonJS and ESmodules
|
||||
entry: ['index.ts'],
|
||||
format: ['cjs', 'esm'], // Build for commonJS and ESmodules
|
||||
dts: true, // Generate declaration file (.d.ts)
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
|
||||
Reference in New Issue
Block a user