UNPKG

@segment/analytics-next

Version:

Analytics Next (aka Analytics 2.0) is the latest version of Segment’s JavaScript SDK - enabling you to send your data to any tool without having to learn, test, or use a new API every time.

300 lines (246 loc) 6.96 kB
import { v4 as uuid } from '@lukeed/uuid' import autoBind from '../../lib/bind-all' import { Traits } from '../events' import { CookieOptions, UniversalStorage, MemoryStorage, StorageObject, StorageSettings, StoreType, applyCookieOptions, initializeStorages, isArrayOfStoreType, } from '../storage' export type ID = string | null | undefined export interface UserOptions { /** * Disables storing any data about the user. */ disable?: boolean localStorageFallbackDisabled?: boolean persist?: boolean cookie?: { key?: string oldKey?: string } localStorage?: { key: string } /** * Store priority * @example stores: [StoreType.Cookie, StoreType.Memory] */ storage?: StorageSettings } const defaults = { persist: true, cookie: { key: 'ajs_user_id', oldKey: 'ajs_user', }, localStorage: { key: 'ajs_user_traits', }, } export interface WithId { id(id?: ID): ID } export class User implements WithId { static defaults = defaults private idKey: string private traitsKey: string private anonKey: string private cookieOptions?: CookieOptions private legacyUserStore: UniversalStorage<{ [k: string]: | { id?: string traits?: Traits } | string }> private traitsStore: UniversalStorage<{ [k: string]: Traits }> private identityStore: UniversalStorage<{ [k: string]: string }> options: UserOptions = {} constructor(options: UserOptions = defaults, cookieOptions?: CookieOptions) { this.options = { ...defaults, ...options } this.cookieOptions = cookieOptions this.idKey = options.cookie?.key ?? defaults.cookie.key this.traitsKey = options.localStorage?.key ?? defaults.localStorage.key this.anonKey = 'ajs_anonymous_id' this.identityStore = this.createStorage(this.options, cookieOptions) // using only cookies for legacy user store this.legacyUserStore = this.createStorage( this.options, cookieOptions, (s) => s === StoreType.Cookie ) // using only localStorage / memory for traits store this.traitsStore = this.createStorage( this.options, cookieOptions, (s) => s !== StoreType.Cookie ) const legacyUser = this.legacyUserStore.get(defaults.cookie.oldKey) if (legacyUser && typeof legacyUser === 'object') { legacyUser.id && this.id(legacyUser.id) legacyUser.traits && this.traits(legacyUser.traits) } autoBind(this) } id = (id?: ID): ID => { if (this.options.disable) { return null } const prevId = this.identityStore.getAndSync(this.idKey) if (id !== undefined) { this.identityStore.set(this.idKey, id) const changingIdentity = id !== prevId && prevId !== null && id !== null if (changingIdentity) { this.anonymousId(null) } } const retId = this.identityStore.getAndSync(this.idKey) if (retId) return retId const retLeg = this.legacyUserStore.get(defaults.cookie.oldKey) return retLeg ? (typeof retLeg === 'object' ? retLeg.id : retLeg) : null } private legacySIO(): [string, string] | null { const val = this.legacyUserStore.get('_sio') as string if (!val) { return null } const [anon, user] = val.split('----') return [anon, user] } anonymousId = (id?: ID): ID => { if (this.options.disable) { return null } if (id === undefined) { const val = this.identityStore.getAndSync(this.anonKey) ?? this.legacySIO()?.[0] if (val) { return val } } if (id === null) { this.identityStore.set(this.anonKey, null) return this.identityStore.getAndSync(this.anonKey) } this.identityStore.set(this.anonKey, id ?? uuid()) return this.identityStore.getAndSync(this.anonKey) } traits = (traits?: Traits | null): Traits | undefined => { if (this.options.disable) { return } if (traits === null) { traits = {} } if (traits) { this.traitsStore.set(this.traitsKey, traits ?? {}) } return this.traitsStore.get(this.traitsKey) ?? {} } identify(id?: ID, traits?: Traits): void { if (this.options.disable) { return } traits = traits ?? {} const currentId = this.id() if (currentId === null || currentId === id) { traits = { ...this.traits(), ...traits, } } if (id) { this.id(id) } this.traits(traits) } logout(): void { this.anonymousId(null) this.id(null) this.traits({}) } reset(): void { this.logout() this.identityStore.clear(this.idKey) this.identityStore.clear(this.anonKey) this.traitsStore.clear(this.traitsKey) } load(): User { return new User(this.options, this.cookieOptions) } save(): boolean { return true } /** * Creates the right storage system applying all the user options, cookie options and particular filters * @param options UserOptions * @param cookieOpts CookieOptions * @param filterStores filter function to apply to any StoreTypes (skipped if options specify using a custom storage) * @returns a Storage object */ private createStorage<T extends StorageObject = StorageObject>( options: UserOptions, cookieOpts?: CookieOptions, filterStores?: (value: StoreType) => boolean ): UniversalStorage<T> { let stores: StoreType[] = [ StoreType.LocalStorage, StoreType.Cookie, StoreType.Memory, ] // If disabled we won't have any storage functionality if (options.disable) { return new UniversalStorage<T>([]) } // If persistance is disabled we will always fallback to Memory Storage if (!options.persist) { return new UniversalStorage<T>([new MemoryStorage<T>()]) } if (options.storage !== undefined && options.storage !== null) { if (isArrayOfStoreType(options.storage)) { // If the user only specified order of stores we will still apply filters and transformations e.g. not using localStorage if localStorageFallbackDisabled stores = options.storage.stores } } // Disable LocalStorage if (options.localStorageFallbackDisabled) { stores = stores.filter((s) => s !== StoreType.LocalStorage) } // Apply Additional filters if (filterStores) { stores = stores.filter(filterStores) } return new UniversalStorage( initializeStorages(applyCookieOptions(stores, cookieOpts)) ) } } const groupDefaults: UserOptions = { persist: true, cookie: { key: 'ajs_group_id', }, localStorage: { key: 'ajs_group_properties', }, } export class Group extends User { constructor(options: UserOptions = groupDefaults, cookie?: CookieOptions) { super({ ...groupDefaults, ...options }, cookie) autoBind(this) } anonymousId = (_id?: ID): ID => { return undefined } }