added tooling (eslint, typescript and prettier)

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-11-02 12:14:37 +01:00
parent 575b3c23bf
commit 493e1b7650
82 changed files with 1890 additions and 1363 deletions

View File

@@ -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();
}
}

View File

@@ -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"
}

View File

@@ -13,9 +13,9 @@
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"outDir": "dist",
"allowImportingTsExtensions": false,
"noEmit": false,
"noEmit": false
}
}

View File

@@ -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,

View File

@@ -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';
}

View File

@@ -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"
}

View File

@@ -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,