From c6b6f23c7cb83552b138a8a6fd3772ab2f3c49ed Mon Sep 17 00:00:00 2001 From: Sunita Prajapati Date: Fri, 25 Apr 2025 16:03:23 +0530 Subject: [PATCH 1/2] feat: Ability to programmatically disable tracking --- packages/core/src/analytics.ts | 23 +++++++-- packages/core/src/storage/sovranStorage.ts | 47 +++++++++++++++++++ packages/core/src/storage/types.ts | 3 ++ .../core/src/test-helpers/mockSegmentStore.ts | 20 ++++++++ .../src/test-helpers/setupSegmentClient.ts | 1 + 5 files changed, 89 insertions(+), 5 deletions(-) diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 9b88c546..b253a162 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,12 +486,15 @@ export class SegmentClient { async process(incomingEvent: SegmentEvent, enrichment?: EnrichmentClosure) { const event = this.applyRawEventData(incomingEvent); event.enrichment = enrichment; - - if (this.isReady.value) { - return this.startTimelineProcessing(event); + if (this.enabled.get() === true) { + if (this.isReady.value) { + return this.startTimelineProcessing(event); + } else { + this.store.pendingEvents.add(event); + return event; + } } else { - this.store.pendingEvents.add(event); - return event; + return; } } 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, }); From 5dba5c357403743310eb8ef8ed42e732583330e8 Mon Sep 17 00:00:00 2001 From: Sunita Prajapati Date: Tue, 29 Apr 2025 10:38:01 +0530 Subject: [PATCH 2/2] refactor: deep nested conditions --- packages/core/src/analytics.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index b253a162..e6caf781 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -486,16 +486,15 @@ export class SegmentClient { async process(incomingEvent: SegmentEvent, enrichment?: EnrichmentClosure) { const event = this.applyRawEventData(incomingEvent); event.enrichment = enrichment; - if (this.enabled.get() === true) { - if (this.isReady.value) { - return this.startTimelineProcessing(event); - } else { - this.store.pendingEvents.add(event); - return event; - } - } else { + if (this.enabled.get() === false) { return; } + if (this.isReady.value) { + return this.startTimelineProcessing(event); + } else { + this.store.pendingEvents.add(event); + return event; + } } /**