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.

656 lines (576 loc) 17.5 kB
import { AliasParams, DispatchedEvent, EventParams, GroupParams, PageParams, resolveAliasArguments, resolveArguments, resolvePageArguments, resolveUserArguments, IdentifyParams, } from '../arguments-resolver' import type { FormArgs, LinkArgs } from '../auto-track' import { isOffline } from '../connection' import { Context } from '../context' import { dispatch } from '@segment/analytics-core' import { Emitter } from '@segment/analytics-generic-utils' import { Callback, EventFactory, EventProperties, SegmentEvent, } from '../events' import { isDestinationPluginWithAddMiddleware, Plugin } from '../plugin' import { EventQueue } from '../queue/event-queue' import { Group, ID, User } from '../user' import autoBind from '../../lib/bind-all' import { PersistedPriorityQueue } from '../../lib/priority-queue/persisted' import type { LegacyIntegration } from '../../plugins/ajs-destination/types' import type { DestinationMiddlewareFunction, MiddlewareFunction, } from '../../plugins/middleware' import { version } from '../../generated/version' import { PriorityQueue } from '../../lib/priority-queue' import { getGlobal } from '../../lib/get-global' import { AnalyticsClassic, AnalyticsCore } from './interfaces' import type { CDNSettings } from '../../browser' import { CookieOptions, MemoryStorage, UniversalStorage, StoreType, applyCookieOptions, initializeStorages, isArrayOfStoreType, } from '../storage' import { setGlobalAnalytics } from '../../lib/global-analytics-helper' import { popPageContext } from '../buffer' import { isSegmentPlugin, SegmentIOPluginMetadata, } from '../../plugins/segmentio' import { AnalyticsSettings, IntegrationsInitOptions, InitOptions, } from '../../browser/settings' export type { InitOptions, AnalyticsSettings } const deprecationWarning = 'This is being deprecated and will be not be available in future releases of Analytics JS' // reference any pre-existing "analytics" object so a user can restore the reference const global: any = getGlobal() const _analytics = global?.analytics function createDefaultQueue( name: string, retryQueue = false, disablePersistance = false ) { const maxAttempts = retryQueue ? 10 : 1 const priorityQueue = disablePersistance ? new PriorityQueue(maxAttempts, []) : new PersistedPriorityQueue(maxAttempts, name) return new EventQueue(priorityQueue) } /** * The public settings that are set on the analytics instance */ export class AnalyticsInstanceSettings { readonly writeKey: string /** * This is an unstable API, it may change in the future without warning. */ readonly cdnSettings: CDNSettings readonly cdnURL?: string get apiHost(): string | undefined { return this._getSegmentPluginMetadata?.()?.apiHost } private _getSegmentPluginMetadata?: () => SegmentIOPluginMetadata | undefined /** * Auto-track specific timeout setting for legacy purposes. */ timeout = 300 constructor(settings: AnalyticsSettings, queue: EventQueue) { this._getSegmentPluginMetadata = () => queue.plugins.find(isSegmentPlugin)?.metadata this.writeKey = settings.writeKey // this is basically just to satisfy typescript / so we don't need to change the function sig of every test. // when loadAnalytics is called, cdnSettings will always be available. const emptyCDNSettings: CDNSettings = { integrations: { 'Segment.io': { apiKey: '', }, }, } this.cdnSettings = settings.cdnSettings ?? emptyCDNSettings this.cdnURL = settings.cdnURL } } /* analytics-classic stubs */ function _stub(this: never) { console.warn(deprecationWarning) } export class Analytics extends Emitter implements AnalyticsCore, AnalyticsClassic { settings: AnalyticsInstanceSettings private _user: User private _group: Group private eventFactory: EventFactory private _debug = false private _universalStorage: UniversalStorage initialized = false integrations: IntegrationsInitOptions options: InitOptions queue: EventQueue constructor( settings: AnalyticsSettings, options?: InitOptions, queue?: EventQueue, user?: User, group?: Group ) { super() const cookieOptions = options?.cookie const disablePersistance = options?.disableClientPersistence ?? false this.queue = queue ?? createDefaultQueue( `${settings.writeKey}:event-queue`, options?.retryQueue, disablePersistance ) this.settings = new AnalyticsInstanceSettings(settings, this.queue) const storageSetting = options?.storage this._universalStorage = this.createStore( disablePersistance, storageSetting, cookieOptions ) this._user = user ?? new User( { persist: !disablePersistance, storage: options?.storage, // Any User specific options override everything else ...options?.user, }, cookieOptions ).load() this._group = group ?? new Group( { persist: !disablePersistance, storage: options?.storage, // Any group specific options override everything else ...options?.group, }, cookieOptions ).load() this.eventFactory = new EventFactory(this._user) this.integrations = options?.integrations ?? {} this.options = options ?? {} autoBind(this) } user = (): User => { return this._user } /** * Creates the storage system based on the settings received * @returns Storage */ private createStore( disablePersistance: boolean, storageSetting: InitOptions['storage'], cookieOptions?: CookieOptions | undefined ): UniversalStorage { // DisablePersistance option overrides all, no storage will be used outside of memory even if specified if (disablePersistance) { return new UniversalStorage([new MemoryStorage()]) } else { if (storageSetting) { if (isArrayOfStoreType(storageSetting)) { // We will create the store with the priority for customer settings return new UniversalStorage( initializeStorages( applyCookieOptions(storageSetting.stores, cookieOptions) ) ) } } } // We default to our multi storage with priority return new UniversalStorage( initializeStorages([ StoreType.LocalStorage, { name: StoreType.Cookie, settings: cookieOptions, }, StoreType.Memory, ]) ) } get storage(): UniversalStorage { return this._universalStorage } async track(...args: EventParams): Promise<DispatchedEvent> { const pageCtx = popPageContext(args) const [name, data, opts, cb] = resolveArguments(...args) const segmentEvent = this.eventFactory.track( name, data as EventProperties, opts, this.integrations, pageCtx ) return this._dispatch(segmentEvent, cb).then((ctx) => { this.emit('track', name, ctx.event.properties, ctx.event.options) return ctx }) } async page(...args: PageParams): Promise<DispatchedEvent> { const pageCtx = popPageContext(args) const [category, page, properties, options, callback] = resolvePageArguments(...args) const segmentEvent = this.eventFactory.page( category, page, properties, options, this.integrations, pageCtx ) return this._dispatch(segmentEvent, callback).then((ctx) => { this.emit('page', category, page, ctx.event.properties, ctx.event.options) return ctx }) } async identify(...args: IdentifyParams): Promise<DispatchedEvent> { const pageCtx = popPageContext(args) const [id, _traits, options, callback] = resolveUserArguments(this._user)( ...args ) this._user.identify(id, _traits) const segmentEvent = this.eventFactory.identify( this._user.id(), this._user.traits(), options, this.integrations, pageCtx ) return this._dispatch(segmentEvent, callback).then((ctx) => { this.emit( 'identify', ctx.event.userId, ctx.event.traits, ctx.event.options ) return ctx }) } group(): Group group(...args: GroupParams): Promise<DispatchedEvent> group(...args: GroupParams): Promise<DispatchedEvent> | Group { const pageCtx = popPageContext(args) if (args.length === 0) { return this._group } const [id, _traits, options, callback] = resolveUserArguments(this._group)( ...args ) this._group.identify(id, _traits) const groupId = this._group.id() const groupTraits = this._group.traits() const segmentEvent = this.eventFactory.group( groupId, groupTraits, options, this.integrations, pageCtx ) return this._dispatch(segmentEvent, callback).then((ctx) => { this.emit('group', ctx.event.groupId, ctx.event.traits, ctx.event.options) return ctx }) } async alias(...args: AliasParams): Promise<DispatchedEvent> { const pageCtx = popPageContext(args) const [to, from, options, callback] = resolveAliasArguments(...args) const segmentEvent = this.eventFactory.alias( to, from, options, this.integrations, pageCtx ) return this._dispatch(segmentEvent, callback).then((ctx) => { this.emit('alias', to, from, ctx.event.options) return ctx }) } async screen(...args: PageParams): Promise<DispatchedEvent> { const pageCtx = popPageContext(args) const [category, page, properties, options, callback] = resolvePageArguments(...args) const segmentEvent = this.eventFactory.screen( category, page, properties, options, this.integrations, pageCtx ) return this._dispatch(segmentEvent, callback).then((ctx) => { this.emit( 'screen', category, page, ctx.event.properties, ctx.event.options ) return ctx }) } async trackClick(...args: LinkArgs): Promise<Analytics> { const autotrack = await import( /* webpackChunkName: "auto-track" */ '../auto-track' ) return autotrack.link.call(this, ...args) } async trackLink(...args: LinkArgs): Promise<Analytics> { const autotrack = await import( /* webpackChunkName: "auto-track" */ '../auto-track' ) return autotrack.link.call(this, ...args) } async trackSubmit(...args: FormArgs): Promise<Analytics> { const autotrack = await import( /* webpackChunkName: "auto-track" */ '../auto-track' ) return autotrack.form.call(this, ...args) } async trackForm(...args: FormArgs): Promise<Analytics> { const autotrack = await import( /* webpackChunkName: "auto-track" */ '../auto-track' ) return autotrack.form.call(this, ...args) } async register(...plugins: Plugin[]): Promise<Context> { const ctx = Context.system() const registrations = plugins.map((xt) => this.queue.register(ctx, xt, this) ) await Promise.all(registrations) return ctx } async deregister(...plugins: string[]): Promise<Context> { const ctx = Context.system() const deregistrations = plugins.map((pl) => { const plugin = this.queue.plugins.find((p) => p.name === pl) if (plugin) { return this.queue.deregister(ctx, plugin, this) } else { ctx.log('warn', `plugin ${pl} not found`) } }) await Promise.all(deregistrations) return ctx } debug(toggle: boolean): Analytics { // Make sure legacy ajs debug gets turned off if it was enabled before upgrading. if (toggle === false && localStorage.getItem('debug')) { localStorage.removeItem('debug') } this._debug = toggle return this } reset(): void { this._user.reset() this._group.reset() this.emit('reset') } timeout(timeout: number): void { this.settings.timeout = timeout } private async _dispatch( event: SegmentEvent, callback?: Callback ): Promise<DispatchedEvent> { const ctx = new Context(event) ctx.stats.increment('analytics_js.invoke', 1, [event.type]) if (isOffline() && !this.options.retryQueue) { return ctx } return dispatch(ctx, this.queue, this, { callback, debug: this._debug, timeout: this.settings.timeout, }) } async addSourceMiddleware(fn: MiddlewareFunction): Promise<Analytics> { await this.queue.criticalTasks.run(async () => { const { sourceMiddlewarePlugin } = await import( /* webpackChunkName: "middleware" */ '../../plugins/middleware' ) const integrations: Record<string, boolean> = {} this.queue.plugins.forEach((plugin) => { if (plugin.type === 'destination') { return (integrations[plugin.name] = true) } }) const plugin = sourceMiddlewarePlugin(fn, integrations) await this.register(plugin) }) return this } /* TODO: This does not have to return a promise? */ addDestinationMiddleware( integrationName: string, ...middlewares: DestinationMiddlewareFunction[] ): Promise<Analytics> { this.queue.plugins .filter(isDestinationPluginWithAddMiddleware) .forEach((p) => { if ( integrationName === '*' || p.name.toLowerCase() === integrationName.toLowerCase() ) { p.addMiddleware(...middlewares) } }) return Promise.resolve(this) } setAnonymousId(id?: string): ID { return this._user.anonymousId(id) } async queryString(query: string): Promise<Context[]> { if (this.options.useQueryString === false) { return [] } const { queryString } = await import( /* webpackChunkName: "queryString" */ '../query-string' ) return queryString(this, query) } /** * @deprecated This function does not register a destination plugin. * * Instantiates a legacy Analytics.js destination. * * This function does not register the destination as an Analytics.JS plugin, * all the it does it to invoke the factory function back. */ use(legacyPluginFactory: (analytics: Analytics) => void): Analytics { legacyPluginFactory(this) return this } async ready( callback: Function = (res: Promise<unknown>[]): Promise<unknown>[] => res ): Promise<unknown> { return Promise.all( this.queue.plugins.map((i) => (i.ready ? i.ready() : Promise.resolve())) ).then((res) => { callback(res) return res }) } // analytics-classic api noConflict(): Analytics { console.warn(deprecationWarning) setGlobalAnalytics(_analytics ?? this) return this } normalize(msg: SegmentEvent): SegmentEvent { console.warn(deprecationWarning) return this.eventFactory['normalize'](msg) } get failedInitializations(): string[] { console.warn(deprecationWarning) return this.queue.failedInitializations } get VERSION(): string { return version } /* @deprecated - noop */ async initialize( _settings?: AnalyticsSettings, _options?: InitOptions ): Promise<Analytics> { console.warn(deprecationWarning) return Promise.resolve(this) } init = this.initialize.bind(this) async pageview(url: string): Promise<Analytics> { console.warn(deprecationWarning) await this.page({ path: url }) return this } get plugins() { console.warn(deprecationWarning) // @ts-expect-error return this._plugins ?? {} } get Integrations() { console.warn(deprecationWarning) const integrations = this.queue.plugins .filter((plugin) => plugin.type === 'destination') .reduce((acc, plugin) => { const name = `${plugin.name .toLowerCase() .replace('.', '') .split(' ') .join('-')}Integration` // @ts-expect-error const integration = window[name] as | (LegacyIntegration & { Integration?: LegacyIntegration }) | undefined if (!integration) { return acc } const nested = integration.Integration // hack - Google Analytics function resides in the "Integration" field if (nested) { acc[plugin.name] = nested return acc } acc[plugin.name] = integration as LegacyIntegration return acc }, {} as Record<string, LegacyIntegration>) return integrations } log = _stub addIntegrationMiddleware = _stub listeners = _stub addEventListener = _stub removeAllListeners = _stub removeListener = _stub removeEventListener = _stub hasListeners = _stub add = _stub addIntegration = _stub // snippet function // eslint-disable-next-line @typescript-eslint/no-explicit-any push(args: any[]) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const an = this as any const method = args.shift() if (method) { if (!an[method]) return } an[method].apply(this, args) } } /** * @returns a no-op analytics instance that does not create cookies or localstorage, or send any events to segment. */ export class NullAnalytics extends Analytics { constructor() { super({ writeKey: '' }, { disableClientPersistence: true }) this.initialized = true } }