diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 9b88c546..e6caf781 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -115,6 +115,10 @@ export class SegmentClient { * Observable to know when the client is fully initialized and ready to send events to destination */ readonly isReady = new Observable(false); + /** + * Access or subscribe to client enabled + */ + readonly enabled: Watchable & Settable; /** * Access or subscribe to client context */ @@ -247,6 +251,12 @@ export class SegmentClient { 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) { @@ -476,7 +486,9 @@ export class SegmentClient { 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 { diff --git a/packages/core/src/storage/sovranStorage.ts b/packages/core/src/storage/sovranStorage.ts index a4e1c98f..ff308a02 100644 --- a/packages/core/src/storage/sovranStorage.ts +++ b/packages/core/src/storage/sovranStorage.ts @@ -40,6 +40,7 @@ type Data = { userInfo: UserInfoState; filters: DestinationFilters; pendingEvents: SegmentEvent[]; + enabled: boolean; }; const INITIAL_VALUES: Data = { @@ -54,6 +55,7 @@ const INITIAL_VALUES: Data = { traits: undefined, }, pendingEvents: [], + enabled: true, }; const isEverythingReady = (state: ReadinessStore) => @@ -185,6 +187,9 @@ export class SovranStorage implements Storage { Settable & Queue; + readonly enabledStore: Store<{ enabled: boolean }>; + readonly enabled: Watchable & Settable; + constructor(config: StorageConfig) { this.storeId = config.storeId; this.storePersistor = config.storePersistor; @@ -195,6 +200,7 @@ export class SovranStorage implements Storage { hasRestoredUserInfo: false, hasRestoredFilters: false, hasRestoredPendingEvents: false, + hasRestoredEnabled: false, }); const markAsReadyGenerator = (key: keyof ReadinessStore) => () => { @@ -490,6 +496,47 @@ export class SovranStorage implements Storage { this.deepLinkStore.subscribe(callback), }; + this.enabledStore = createStore( + { enabled: INITIAL_VALUES.enabled }, + { + persist: { + storeId: `${this.storeId}-enabled`, + persistor: this.storePersistor, + saveDelay: this.storePersistorSaveDelay, + onInitialized: markAsReadyGenerator('hasRestoredEnabled'), + }, + } + ); + // Accessor object for enabled + this.enabled = { + get: createGetter( + () => { + const state = this.enabledStore.getState(); + return state.enabled; + }, + async () => { + const value = await this.enabledStore.getState(true); + return value.enabled; + } + ), + + onChange: (callback: (value: boolean) => void) => { + return this.enabledStore.subscribe((store) => { + callback(store.enabled); + }); + }, + + set: async (value: boolean | ((prev: boolean) => boolean)) => { + const { enabled } = await this.enabledStore.dispatch((state) => { + const newEnabled = + value instanceof Function ? value(state.enabled) : value; + return { enabled: newEnabled }; + }); + return enabled; + }, + }; + + this.fixAnonymousId(); } diff --git a/packages/core/src/storage/types.ts b/packages/core/src/storage/types.ts index dcf2b7fe..162398ee 100644 --- a/packages/core/src/storage/types.ts +++ b/packages/core/src/storage/types.ts @@ -59,6 +59,7 @@ export interface ReadinessStore { hasRestoredUserInfo: boolean; hasRestoredFilters: boolean; hasRestoredPendingEvents: boolean; + hasRestoredEnabled: boolean; } /** @@ -91,6 +92,8 @@ export interface Storage { readonly pendingEvents: Watchable & Settable & Queue; + + readonly enabled: Watchable & Settable; } export type DeepLinkData = { referring_application: string; diff --git a/packages/core/src/test-helpers/mockSegmentStore.ts b/packages/core/src/test-helpers/mockSegmentStore.ts index 983507e1..2b16f2bb 100644 --- a/packages/core/src/test-helpers/mockSegmentStore.ts +++ b/packages/core/src/test-helpers/mockSegmentStore.ts @@ -24,6 +24,7 @@ import { createGetter } from '../storage/helpers'; export type StoreData = { isReady: boolean; + enabled: boolean; context?: DeepPartial; settings: SegmentAPIIntegrations; consentSettings?: SegmentAPIConsentSettings; @@ -36,6 +37,7 @@ export type StoreData = { const INITIAL_VALUES: StoreData = { isReady: true, + enabled: true, context: undefined, settings: { [SEGMENT_DESTINATION_KEY]: {}, @@ -90,6 +92,7 @@ export class MockSegmentStore implements Storage { userInfo: createCallbackManager(), deepLinkData: createCallbackManager(), pendingEvents: createCallbackManager(), + enabled: createCallbackManager(), }; readonly isReady = { @@ -103,6 +106,23 @@ export class MockSegmentStore implements Storage { }, }; + readonly enabled = { + get: createMockStoreGetter(() => { + return this.data.enabled; + }), + onChange: (_callback: (value: boolean) => void) => { + return () => { + return; + }; + }, + set: async (value: boolean | ((prev: boolean) => boolean)) => { + this.data.enabled = + value instanceof Function ? value(this.data.enabled ?? true) : value; + this.callbacks.enabled.run(this.data.enabled); + return this.data.enabled; + }, + }; + readonly context: Watchable | undefined> & Settable> = { get: createMockStoreGetter(() => ({ ...this.data.context })), diff --git a/packages/core/src/test-helpers/setupSegmentClient.ts b/packages/core/src/test-helpers/setupSegmentClient.ts index 6eb00db1..a6426274 100644 --- a/packages/core/src/test-helpers/setupSegmentClient.ts +++ b/packages/core/src/test-helpers/setupSegmentClient.ts @@ -14,6 +14,7 @@ export const createTestClient = ( ) => { const store = new MockSegmentStore({ isReady: true, + enabled: true, ...storeData, });