@segment/analytics-react-native
Version:
The hassle-free way to add Segment analytics to your React-Native app.
1,029 lines (900 loc) • 29.6 kB
text/typescript
import type { Rule } from '@segment/tsub/dist/store';
import deepmerge from 'deepmerge';
import {
AppState,
AppStateStatus,
NativeEventSubscription,
} from 'react-native';
import {
settingsCDN,
workspaceDestinationFilterKey,
defaultFlushInterval,
defaultFlushAt,
} from './constants';
import { getContext } from './context';
import {
createAliasEvent,
createGroupEvent,
createIdentifyEvent,
createScreenEvent,
createTrackEvent,
} from './events';
import {
CountFlushPolicy,
Observable,
TimerFlushPolicy,
} from './flushPolicies';
import { FlushPolicyExecuter } from './flushPolicies/flush-policy-executer';
import { DestinationPlugin, PlatformPlugin, Plugin } from './plugin';
import { SegmentDestination } from './plugins/SegmentDestination';
import {
createGetter,
DeepLinkData,
Settable,
Storage,
Watchable,
} from './storage';
import { Timeline } from './timeline';
import {
DestinationFilters,
EventType,
SegmentAPISettings,
SegmentAPIConsentSettings,
EdgeFunctionSettings,
EnrichmentClosure,
} from './types';
import {
Config,
Context,
DeepPartial,
GroupTraits,
IntegrationSettings,
JsonMap,
LoggerType,
PluginType,
SegmentAPIIntegrations,
SegmentEvent,
UserInfoState,
UserTraits,
} from './types';
import {
allSettled,
getPluginsWithFlush,
getPluginsWithReset,
getURL,
} from './util';
import { getUUID } from './uuid';
import type { FlushPolicy } from './flushPolicies';
import {
checkResponseForErrors,
ErrorType,
SegmentError,
translateHTTPError,
} from './errors';
import { QueueFlushingPlugin } from './plugins/QueueFlushingPlugin';
type OnPluginAddedCallback = (plugin: Plugin) => void;
export class SegmentClient {
// the config parameters for the client - a merge of user provided and default options
private config: Config;
// Storage
private store: Storage;
// current app state
private appState: AppStateStatus | 'unknown' = 'unknown';
// subscription for propagating changes to appState
private appStateSubscription?: NativeEventSubscription;
// logger
public logger: LoggerType;
// whether the user has called cleanup
private destroyed = false;
private isAddingPlugins = false;
private timeline: Timeline;
private pluginsToAdd: Plugin[] = [];
private flushPolicyExecuter: FlushPolicyExecuter = new FlushPolicyExecuter(
[],
() => {
void this.flush();
}
);
private onPluginAddedObservers: OnPluginAddedCallback[] = [];
private readonly platformPlugins: PlatformPlugin[] = [];
// Watchables
/**
* Observable to know when the client is fully initialized and ready to send events to destination
*/
readonly isReady = new Observable<boolean>(false);
/**
* Access or subscribe to client enabled
*/
readonly enabled: Watchable<boolean> & Settable<boolean>;
/**
* Access or subscribe to client context
*/
readonly context: Watchable<DeepPartial<Context> | undefined> &
Settable<DeepPartial<Context>>;
/**
* Access or subscribe to adTrackingEnabled (also accesible from context)
*/
readonly adTrackingEnabled: Watchable<boolean>;
/**
* Access or subscribe to integration settings
*/
readonly settings: Watchable<SegmentAPIIntegrations | undefined>;
/**
* Access or subscribe to integration settings
*/
readonly consentSettings: Watchable<SegmentAPIConsentSettings | undefined>;
/**
* Access or subscribe to edge functions settings
*/
readonly edgeFunctionSettings: Watchable<EdgeFunctionSettings | undefined>;
/**
* Access or subscribe to destination filter settings
*/
readonly filters: Watchable<DestinationFilters | undefined>;
/**
* Access or subscribe to user info (anonymousId, userId, traits)
*/
readonly userInfo: Watchable<UserInfoState> & Settable<UserInfoState>;
readonly deepLinkData: Watchable<DeepLinkData>;
// private telemetry?: Telemetry;
/**
* Returns the plugins currently loaded in the timeline
* @param ofType Type of plugins, defaults to all
* @returns List of Plugin objects
*/
getPlugins(ofType?: PluginType): readonly Plugin[] {
const plugins = { ...this.timeline.plugins };
if (ofType !== undefined) {
return [...(plugins[ofType] ?? [])];
}
return [
...this.getPlugins(PluginType.before),
...this.getPlugins(PluginType.enrichment),
...this.getPlugins(PluginType.utility),
...this.getPlugins(PluginType.destination),
...this.getPlugins(PluginType.after),
];
}
/**
* Retrieves a copy of the current client configuration
*/
getConfig() {
return { ...this.config };
}
constructor({
config,
logger,
store,
}: {
config: Config;
logger: LoggerType;
store: Storage;
}) {
this.logger = logger;
this.config = config;
this.store = store;
this.timeline = new Timeline();
// Initialize the watchables
this.context = {
get: this.store.context.get,
set: this.store.context.set,
onChange: this.store.context.onChange,
};
this.adTrackingEnabled = {
get: createGetter(
() => this.store.context.get()?.device?.adTrackingEnabled ?? false,
async () => {
const context = await this.store.context.get(true);
return context?.device?.adTrackingEnabled ?? false;
}
),
onChange: (callback: (value: boolean) => void) =>
this.store.context.onChange((context?: DeepPartial<Context>) => {
callback(context?.device?.adTrackingEnabled ?? false);
}),
};
this.settings = {
get: this.store.settings.get,
onChange: this.store.settings.onChange,
};
this.consentSettings = {
get: this.store.consentSettings.get,
onChange: this.store.consentSettings.onChange,
};
this.edgeFunctionSettings = {
get: this.store.edgeFunctionSettings.get,
onChange: this.store.edgeFunctionSettings.onChange,
};
this.filters = {
get: this.store.filters.get,
onChange: this.store.filters.onChange,
};
this.userInfo = {
get: this.store.userInfo.get,
set: this.store.userInfo.set,
onChange: this.store.userInfo.onChange,
};
this.deepLinkData = {
get: this.store.deepLinkData.get,
onChange: this.store.deepLinkData.onChange,
};
this.enabled = {
get: this.store.enabled.get,
set: this.store.enabled.set,
onChange: this.store.enabled.onChange,
};
// add segment destination plugin unless
// asked not to via configuration.
if (this.config.autoAddSegmentDestination === true) {
const segmentDestination = new SegmentDestination();
this.add({ plugin: segmentDestination });
}
// Setup platform specific plugins
this.platformPlugins.forEach((plugin) => this.add({ plugin: plugin }));
// set up tracking for lifecycle events
this.setupLifecycleEvents();
}
// Watch for isReady so that we can handle any pending events
private async storageReady(): Promise<boolean> {
return new Promise((resolve) => {
this.store.isReady.onChange((value) => {
resolve(value);
});
});
}
/**
* Initializes the client plugins, settings and subscribers.
* Can only be called once.
*/
async init() {
try {
if (this.isReady.value) {
this.logger.warn('SegmentClient already initialized');
return;
}
if ((await this.store.isReady.get(true)) === false) {
await this.storageReady();
}
// Get new settings from segment
// It's important to run this before checkInstalledVersion and trackDeeplinks to give time for destination plugins
// which make use of the settings object to initialize
await this.fetchSettings();
await allSettled([
// save the current installed version
this.checkInstalledVersion(),
// check if the app was opened from a deep link
this.trackDeepLinks(),
]);
await this.onReady();
this.isReady.value = true;
} catch (error) {
this.reportInternalError(
new SegmentError(
ErrorType.InitializationError,
'Client did not initialize correctly',
error
)
);
}
}
private generateFiltersMap(rules: Rule[]): DestinationFilters {
const map: DestinationFilters = {};
for (const r of rules) {
const key = r.destinationName ?? workspaceDestinationFilterKey;
map[key] = r;
}
return map;
}
private getEndpointForSettings(): string {
let settingsPrefix = '';
let settingsEndpoint = '';
const hasProxy = !!(this.config?.cdnProxy ?? '');
const useSegmentEndpoints = Boolean(this.config?.useSegmentEndpoints);
if (hasProxy) {
settingsPrefix = this.config.cdnProxy ?? '';
if (useSegmentEndpoints) {
const isCdnProxyEndsWithSlash = settingsPrefix.endsWith('/');
settingsEndpoint = isCdnProxyEndsWithSlash
? `projects/${this.config.writeKey}/settings`
: `/projects/${this.config.writeKey}/settings`;
}
} else {
settingsPrefix = settingsCDN;
settingsEndpoint = `/${this.config.writeKey}/settings`;
}
try {
return getURL(settingsPrefix, settingsEndpoint);
} catch (error) {
console.error(
'Error in getEndpointForSettings:',
`fallback to ${settingsCDN}/${this.config.writeKey}/settings`
);
return `${settingsCDN}/${this.config.writeKey}/settings`;
}
}
async fetchSettings() {
const settingsURL = this.getEndpointForSettings();
try {
const res = await fetch(settingsURL, {
headers: {
'Cache-Control': 'no-cache',
},
});
checkResponseForErrors(res);
const resJson: SegmentAPISettings =
(await res.json()) as SegmentAPISettings;
const integrations = resJson.integrations;
const consentSettings = resJson.consentSettings;
const edgeFunctionSettings = resJson.edgeFunction;
const filters = this.generateFiltersMap(
resJson.middlewareSettings?.routingRules ?? []
);
this.logger.info('Received settings from Segment succesfully.');
await Promise.all([
this.store.settings.set(integrations),
this.store.consentSettings.set(consentSettings),
this.store.edgeFunctionSettings.set(edgeFunctionSettings),
this.store.filters.set(filters),
]);
} catch (e) {
this.reportInternalError(translateHTTPError(e));
this.logger.warn(
`Could not receive settings from Segment. ${
this.config.defaultSettings
? 'Will use the default settings.'
: 'Device mode destinations will be ignored unless you specify default settings in the client config.'
}`
);
if (this.config.defaultSettings) {
await this.store.settings.set(this.config.defaultSettings.integrations);
}
}
}
/**
* There is no garbage collection in JS, which means that any listeners, timeouts and subscriptions
* would run until the application closes
*
* This method exists in case the user for some reason needs to recreate the class instance during runtime.
* In this case, they should run client.cleanup() to destroy the listeners in the old client before creating a new one.
*
* There is a Stage 3 EMCAScript proposal to add a user-defined finalizer, which we could potentially switch to if
* it gets approved: https://github.com/tc39/proposal-weakrefs#finalizers
*/
cleanup() {
this.flushPolicyExecuter.cleanup();
this.appStateSubscription?.remove();
this.destroyed = true;
}
private setupLifecycleEvents() {
this.appStateSubscription?.remove();
this.appStateSubscription = AppState.addEventListener(
'change',
(nextAppState) => {
this.handleAppStateChange(nextAppState);
}
);
}
/**
Applies the supplied closure to the currently loaded set of plugins.
NOTE: This does not apply to plugins contained within DestinationPlugins.
- Parameter closure: A closure that takes an plugin to be operated on as a parameter.
*/
apply(closure: (plugin: Plugin) => void) {
this.timeline.apply(closure);
}
/**
* Adds a new plugin to the currently loaded set.
* @param {{ plugin: Plugin, settings?: IntegrationSettings }} Plugin to be added. Settings are optional if you want to force a configuration instead of the Segment Cloud received one
*/
add<P extends Plugin>({
plugin,
settings,
}: {
plugin: P;
settings?: P extends DestinationPlugin ? IntegrationSettings : never;
}) {
// plugins can either be added immediately or
// can be cached and added later during the next state update
// this is to avoid adding plugins before network requests made as part of setup have resolved
if (settings !== undefined && plugin.type === PluginType.destination) {
void this.store.settings.add(
(plugin as unknown as DestinationPlugin).key,
settings
);
}
if (!this.isReady.value) {
this.pluginsToAdd.push(plugin);
} else {
this.addPlugin(plugin);
}
}
private addPlugin(plugin: Plugin) {
plugin.configure(this);
this.timeline.add(plugin);
this.triggerOnPluginLoaded(plugin);
}
/**
Removes and unloads plugins with a matching name from the system.
- Parameter pluginName: An plugin name.
*/
remove({ plugin }: { plugin: Plugin }) {
this.timeline.remove(plugin);
}
async process(incomingEvent: SegmentEvent, enrichment?: EnrichmentClosure) {
const event = this.applyRawEventData(incomingEvent);
event.enrichment = enrichment;
if (this.enabled.get() === false) {
return;
}
if (this.isReady.value) {
return this.startTimelineProcessing(event);
} else {
this.store.pendingEvents.add(event);
return event;
}
}
/**
* Starts timeline processing
* @param incomingEvent Segment Event
* @returns Segment Event
*/
private async startTimelineProcessing(
incomingEvent: SegmentEvent
): Promise<SegmentEvent | undefined> {
const event = await this.applyContextData(incomingEvent);
this.flushPolicyExecuter.notify(event);
return this.timeline.process(event);
}
private async trackDeepLinks() {
if (this.getConfig().trackDeepLinks === true) {
const deepLinkProperties = await this.store.deepLinkData.get(true);
this.trackDeepLinkEvent(deepLinkProperties);
this.store.deepLinkData.onChange((data) => {
this.trackDeepLinkEvent(data);
});
}
}
private trackDeepLinkEvent(deepLinkProperties: DeepLinkData) {
if (deepLinkProperties.url !== '') {
const event = createTrackEvent({
event: 'Deep Link Opened',
properties: {
...deepLinkProperties,
},
});
void this.process(event);
this.logger.info('TRACK (Deep Link Opened) event saved', event);
}
}
/**
* Executes when everything in the client is ready for sending events
* @param isReady
*/
private async onReady() {
// Add all plugins awaiting store
if (this.pluginsToAdd.length > 0 && !this.isAddingPlugins) {
this.isAddingPlugins = true;
try {
// start by adding the plugins
this.pluginsToAdd.forEach((plugin) => {
this.addPlugin(plugin);
});
// now that they're all added, clear the cache
// this prevents this block running for every update
this.pluginsToAdd = [];
} finally {
this.isAddingPlugins = false;
}
}
// Start flush policies
// This should be done before any pending events are added to the queue so that any policies that rely on events queued can trigger accordingly
this.setupFlushPolicies();
// Send all events in the queue
const pending = await this.store.pendingEvents.get(true);
for (const e of pending) {
await this.startTimelineProcessing(e);
await this.store.pendingEvents.remove(e);
}
this.flushPolicyExecuter.manualFlush();
}
async flush(): Promise<void> {
try {
if (this.destroyed) {
return;
}
this.flushPolicyExecuter.reset();
const promises: (void | Promise<void>)[] = [];
getPluginsWithFlush(this.timeline).forEach((plugin) => {
promises.push(plugin.flush());
});
const results = await allSettled(promises);
for (const r of results) {
if (r.status === 'rejected') {
this.reportInternalError(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
new SegmentError(ErrorType.FlushError, `Flush failed: ${r.reason}`)
);
}
}
} catch (error) {
this.reportInternalError(
new SegmentError(ErrorType.FlushError, 'Flush failed', error)
);
}
}
async screen(
name: string,
options?: JsonMap,
enrichment?: EnrichmentClosure
) {
const event = createScreenEvent({
name,
properties: options,
});
await this.process(event, enrichment);
this.logger.info('SCREEN event saved', event);
}
async track(
eventName: string,
options?: JsonMap,
enrichment?: EnrichmentClosure
) {
const event = createTrackEvent({
event: eventName,
properties: options,
});
await this.process(event, enrichment);
this.logger.info('TRACK event saved', event);
}
async identify(
userId?: string,
userTraits?: UserTraits,
enrichment?: EnrichmentClosure
) {
const event = createIdentifyEvent({
userId: userId,
userTraits: userTraits,
});
await this.process(event, enrichment);
this.logger.info('IDENTIFY event saved', event);
}
async group(
groupId: string,
groupTraits?: GroupTraits,
enrichment?: EnrichmentClosure
) {
const event = createGroupEvent({
groupId,
groupTraits,
});
await this.process(event, enrichment);
this.logger.info('GROUP event saved', event);
}
async alias(newUserId: string, enrichment?: EnrichmentClosure) {
// We don't use a concurrency safe version of get here as we don't want to lock the values yet,
// we will update the values correctly when InjectUserInfo processes the change
const { anonymousId, userId: previousUserId } = this.store.userInfo.get();
const event = createAliasEvent({
anonymousId,
userId: previousUserId,
newUserId,
});
await this.process(event, enrichment);
this.logger.info('ALIAS event saved', event);
}
/**
* Called once when the client is first created
*
* Detect and save the the currently installed application version
* Send application lifecycle events if trackAppLifecycleEvents is enabled
*
* Exactly one of these events will be sent, depending on the current and previous version:s
* Application Installed - no information on the previous version, so it's a fresh install
* Application Updated - the previous detected version is different from the current version
* Application Opened - the previously detected version is same as the current version
*/
private async checkInstalledVersion() {
const context = await getContext(undefined, this.config);
const previousContext = this.store.context.get();
// Only overwrite the previous context values to preserve any values that are added by enrichment plugins like IDFA
await this.store.context.set(deepmerge(previousContext ?? {}, context));
if (this.config.trackAppLifecycleEvents !== true) {
return;
}
if (previousContext?.app === undefined) {
const event = createTrackEvent({
event: 'Application Installed',
properties: {
version: context.app.version,
build: context.app.build,
},
});
void this.process(event);
this.logger.info('TRACK (Application Installed) event saved', event);
} else if (context.app.version !== previousContext.app.version) {
const event = createTrackEvent({
event: 'Application Updated',
properties: {
version: context.app.version,
build: context.app.build,
previous_version: previousContext.app.version,
previous_build: previousContext.app.build,
},
});
void this.process(event);
this.logger.info('TRACK (Application Updated) event saved', event);
}
const event = createTrackEvent({
event: 'Application Opened',
properties: {
from_background: false,
version: context.app.version,
build: context.app.build,
},
});
void this.process(event);
this.logger.info('TRACK (Application Opened) event saved', event);
}
/**
* AppState event listener. Called whenever the app state changes.
*
* Send application lifecycle events if trackAppLifecycleEvents is enabled.
*
* Application Opened - only when the app state changes from 'inactive' or 'background' to 'active'
* The initial event from 'unknown' to 'active' is handled on launch in checkInstalledVersion
* Application Backgrounded - when the app state changes from 'inactive' or 'background' to 'active
*
* @param nextAppState 'active', 'inactive', 'background' or 'unknown'
*/
private handleAppStateChange(nextAppState: AppStateStatus) {
if (this.config.trackAppLifecycleEvents === true) {
if (
['inactive', 'background'].includes(this.appState) &&
nextAppState === 'active'
) {
const context = this.store.context.get();
const event = createTrackEvent({
event: 'Application Opened',
properties: {
from_background: true,
version: context?.app?.version,
build: context?.app?.build,
},
});
void this.process(event);
this.logger.info('TRACK (Application Opened) event saved', event);
} else if (
(this.appState === 'active' || this.appState === 'unknown') && // Check if appState is 'active' or 'unknown'
['inactive', 'background'].includes(nextAppState)
) {
// Check if next app state is 'inactive' or 'background'
const event = createTrackEvent({
event: 'Application Backgrounded',
});
void this.process(event);
this.logger.info('TRACK (Application Backgrounded) event saved', event);
}
}
this.appState = nextAppState;
}
async reset(resetAnonymousId = true) {
try {
const { anonymousId: currentId } = await this.store.userInfo.get(true);
const anonymousId = resetAnonymousId === true ? getUUID() : currentId;
await this.store.userInfo.set({
anonymousId,
userId: undefined,
traits: undefined,
});
await allSettled(
getPluginsWithReset(this.timeline).map((plugin) => plugin.reset())
);
this.logger.info('Client has been reset');
} catch (error) {
this.reportInternalError(
new SegmentError(ErrorType.ResetError, 'Error during reset', error)
);
}
}
/**
* Registers a callback for each plugin that gets added to the analytics client.
* @param callback Function to call
*/
onPluginLoaded(callback: OnPluginAddedCallback) {
const i = this.onPluginAddedObservers.push(callback);
return () => {
this.onPluginAddedObservers.splice(i, 1);
};
}
private triggerOnPluginLoaded(plugin: Plugin) {
this.onPluginAddedObservers.map((f) => f?.(plugin));
}
/**
* Initializes the flush policies from config and subscribes to updates to
* trigger flush
*/
private setupFlushPolicies() {
const flushPolicies = [];
// If there are zero policies or flushAt/flushInterval use the defaults:
if (this.config.flushPolicies !== undefined) {
flushPolicies.push(...this.config.flushPolicies);
} else {
if (
this.config.flushAt === undefined ||
(this.config.flushAt !== null && this.config.flushAt > 0)
) {
flushPolicies.push(
new CountFlushPolicy(this.config.flushAt ?? defaultFlushAt)
);
}
if (
this.config.flushInterval === undefined ||
(this.config.flushInterval !== null && this.config.flushInterval > 0)
) {
flushPolicies.push(
new TimerFlushPolicy(
(this.config.flushInterval ?? defaultFlushInterval) * 1000
)
);
}
}
for (const fp of flushPolicies) {
this.flushPolicyExecuter.add(fp);
}
}
/**
* Adds a FlushPolicy to the list
* @param policies policies to add
*/
addFlushPolicy(...policies: FlushPolicy[]) {
for (const policy of policies) {
this.flushPolicyExecuter.add(policy);
}
}
/**
* Removes a FlushPolicy from the execution
*
* @param policies policies to remove
* @returns true if the value was removed, false if not found
*/
removeFlushPolicy(...policies: FlushPolicy[]) {
for (const policy of policies) {
this.flushPolicyExecuter.remove(policy);
}
}
/**
* Returns the current enabled flush policies
*/
getFlushPolicies() {
return this.flushPolicyExecuter.policies;
}
reportInternalError(error: SegmentError, fatal = false) {
if (fatal) {
this.logger.error('A critical error ocurred: ', error);
} else {
this.logger.warn('An internal error occurred: ', error);
}
this.config.errorHandler?.(error);
}
/**
* Sets the messageId and timestamp
* @param event Segment Event
* @returns event with data injected
*/
private applyRawEventData = (event: SegmentEvent): SegmentEvent => {
return {
...event,
messageId: getUUID(),
timestamp: new Date().toISOString(),
integrations: event.integrations ?? {},
} as SegmentEvent;
};
/**
* Injects context and userInfo data into the event
* This is handled outside of the timeline to prevent concurrency issues between plugins
* This is only added after the client is ready to let the client restore values from storage
* @param event Segment Event
* @returns event with data injected
*/
private applyContextData = async (
event: SegmentEvent
): Promise<SegmentEvent> => {
const userInfo = await this.processUserInfo(event);
const context = await this.context.get(true);
return {
...event,
...userInfo,
context: {
...event.context,
...context,
},
} as SegmentEvent;
};
/**
* Processes the userInfo to add to an event.
* For Identify and Alias: it saves the new userId and traits into the storage
* For all: set the userId and anonymousId from the current values
* @param event segment event
* @returns userInfo to inject to an event
*/
private processUserInfo = async (
event: SegmentEvent
): Promise<Partial<SegmentEvent>> => {
// Order here is IMPORTANT!
// Identify and Alias userInfo set operations have to come as soon as possible
// Do not block the set by doing a safe get first as it might cause a race condition
// within events procesing in the timeline asyncronously
if (event.type === EventType.IdentifyEvent) {
const userInfo = await this.userInfo.set((state) => ({
...state,
userId: event.userId ?? state.userId,
traits: {
...state.traits,
...event.traits,
},
}));
return {
anonymousId: userInfo.anonymousId,
userId: event.userId ?? userInfo.userId,
traits: {
...userInfo.traits,
...event.traits,
},
};
} else if (event.type === EventType.AliasEvent) {
let previousUserId: string;
const userInfo = await this.userInfo.set((state) => {
previousUserId = state.userId ?? state.anonymousId;
return {
...state,
userId: event.userId,
};
});
return {
anonymousId: userInfo.anonymousId,
userId: event.userId,
previousId: previousUserId!,
};
}
const userInfo = await this.userInfo.get(true);
return {
anonymousId: userInfo.anonymousId,
userId: userInfo.userId,
};
};
/* Method for clearing flush queue */
clear() {
const plugins = this.getPlugins();
plugins.forEach(async (plugin) => {
if (plugin instanceof SegmentDestination) {
const timelinePlugins = plugin.timeline?.plugins?.after ?? [];
for (const subPlugin of timelinePlugins) {
if (subPlugin instanceof QueueFlushingPlugin) {
await subPlugin.dequeueEvents();
}
}
}
});
this.flushPolicyExecuter.reset();
}
/**
* Method to get count of events in flush queue.
*/
async pendingEvents() {
const plugins = this.getPlugins();
let totalEventsCount = 0;
for (const plugin of plugins) {
// We're looking inside SegmentDestination's `after` plugins
if (plugin instanceof SegmentDestination) {
const timelinePlugins = plugin.timeline?.plugins?.after ?? [];
for (const subPlugin of timelinePlugins) {
if (subPlugin instanceof QueueFlushingPlugin) {
const eventsCount = await subPlugin.pendingEvents();
totalEventsCount += eventsCount;
}
}
}
}
return totalEventsCount;
}
}