From e6bc91ae797314097a3cb990a8b128845aec4862 Mon Sep 17 00:00:00 2001 From: Neek Sandhu Date: Thu, 7 Dec 2023 09:40:59 -0800 Subject: [PATCH] Add support for hasUnmappedDestinations --- .../__tests__/__helpers__/mockSegmentStore.ts | 19 ++++++++ packages/core/src/analytics.ts | 13 +++++ packages/core/src/plugins/ConsentPlugin.ts | 22 ++++++--- .../destinationMultipleCategories.test.ts | 1 + .../ConsentNotEnabledAtSegment.json | 1 - .../DestinationsMultipleCategories.json | 2 +- .../mockSettings/NoUnmappedDestinations.json | 2 +- .../mockSettings/UnmappedDestinations.json | 2 +- .../__tests__/consent/noUnmapped.test.ts | 1 + .../__tests__/consent/unmapped.test.ts | 1 + packages/core/src/storage/sovranStorage.ts | 47 +++++++++++++++++++ packages/core/src/storage/types.ts | 4 ++ packages/core/src/types.ts | 6 +++ 13 files changed, 111 insertions(+), 10 deletions(-) diff --git a/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts b/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts index 94624e21..fbdc39c7 100644 --- a/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts +++ b/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts @@ -12,6 +12,7 @@ import type { DestinationFilters, IntegrationSettings, RoutingRule, + SegmentAPIConsentSettings, SegmentAPIIntegrations, UserInfoState, } from '../../types'; @@ -22,6 +23,7 @@ export type StoreData = { isReady: boolean; context?: DeepPartial; settings: SegmentAPIIntegrations; + consentSettings?: SegmentAPIConsentSettings; filters: DestinationFilters; userInfo: UserInfoState; deepLinkData: DeepLinkData; @@ -33,6 +35,7 @@ const INITIAL_VALUES: StoreData = { settings: { [SEGMENT_DESTINATION_KEY]: {}, }, + consentSettings: undefined, filters: {}, userInfo: { anonymousId: 'anonymousId', @@ -71,6 +74,9 @@ export class MockSegmentStore implements Storage { private callbacks = { context: createCallbackManager | undefined>(), settings: createCallbackManager(), + consentSettings: createCallbackManager< + SegmentAPIConsentSettings | undefined + >(), filters: createCallbackManager(), userInfo: createCallbackManager(), deepLinkData: createCallbackManager(), @@ -123,6 +129,19 @@ export class MockSegmentStore implements Storage { }, }; + readonly consentSettings: Watchable & + Settable = { + get: createMockStoreGetter(() => this.data.consentSettings), + onChange: (callback: (value?: SegmentAPIConsentSettings) => void) => + this.callbacks.consentSettings.register(callback), + set: (value) => { + this.data.consentSettings = + value instanceof Function ? value(this.data.consentSettings) : value; + this.callbacks.consentSettings.run(this.data.consentSettings); + return this.data.consentSettings; + }, + }; + readonly filters: Watchable & Settable & Dictionary = { diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 79cc6090..3355e419 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -59,6 +59,7 @@ import { SegmentError, translateHTTPError, } from './errors'; +import type { SegmentAPIConsentSettings } from '.'; type OnPluginAddedCallback = (plugin: Plugin) => void; @@ -116,6 +117,11 @@ export class SegmentClient { */ readonly settings: Watchable; + /** + * Access or subscribe to integration settings + */ + readonly consentSettings: Watchable; + /** * Access or subscribe to destination filter settings */ @@ -198,6 +204,11 @@ export class SegmentClient { onChange: this.store.settings.onChange, }; + this.consentSettings = { + get: this.store.consentSettings.get, + onChange: this.store.consentSettings.onChange, + }; + this.filters = { get: this.store.filters.get, onChange: this.store.filters.onChange, @@ -305,12 +316,14 @@ export class SegmentClient { const resJson: SegmentAPISettings = (await res.json()) as SegmentAPISettings; const integrations = resJson.integrations; + const consentSettings = resJson.consentSettings; 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.filters.set(filters), ]); } catch (e) { diff --git a/packages/core/src/plugins/ConsentPlugin.ts b/packages/core/src/plugins/ConsentPlugin.ts index 1695a851..807b6d90 100644 --- a/packages/core/src/plugins/ConsentPlugin.ts +++ b/packages/core/src/plugins/ConsentPlugin.ts @@ -84,14 +84,14 @@ export class ConsentPlugin extends Plugin { const preferences = event.context?.consent?.categoryPreferences || {}; if (plugin.key === SEGMENT_DESTINATION_KEY) { + const noneConsented = Object.values(preferences).every( + (consented) => !consented + ); + return ( this.isConsentUpdateEvent(event) || - !( - Object.values(preferences).every((consented) => !consented) && - Object.entries(settings) - .filter(([k]) => k !== SEGMENT_DESTINATION_KEY) - .every(([_, v]) => this.containsConsentSettings(v)) - ) + !this.isConsentFeatureSetup() || + !(noneConsented && !this.hasUnmappedDestinations()) ); } @@ -127,6 +127,16 @@ export class ConsentPlugin extends Plugin { private isConsentUpdateEvent(event: SegmentEvent): boolean { return (event as TrackEventType).event === CONSENT_PREF_UPDATE_EVENT; } + + private hasUnmappedDestinations(): boolean { + return ( + this.analytics?.consentSettings.get()?.hasUnmappedDestinations === true + ); + } + + private isConsentFeatureSetup(): boolean { + return typeof this.analytics?.consentSettings.get() === 'object'; + } } /** diff --git a/packages/core/src/plugins/__tests__/consent/destinationMultipleCategories.test.ts b/packages/core/src/plugins/__tests__/consent/destinationMultipleCategories.test.ts index 6b3aa87a..69984c46 100644 --- a/packages/core/src/plugins/__tests__/consent/destinationMultipleCategories.test.ts +++ b/packages/core/src/plugins/__tests__/consent/destinationMultipleCategories.test.ts @@ -13,6 +13,7 @@ describe('Destinations multiple categories', () => { createTestClient( { settings: destinationsMultipleCategories.integrations, + consentSettings: destinationsMultipleCategories.consentSettings, }, { autoAddSegmentDestination: true } ); diff --git a/packages/core/src/plugins/__tests__/consent/mockSettings/ConsentNotEnabledAtSegment.json b/packages/core/src/plugins/__tests__/consent/mockSettings/ConsentNotEnabledAtSegment.json index 343d06c4..eaa07a23 100644 --- a/packages/core/src/plugins/__tests__/consent/mockSettings/ConsentNotEnabledAtSegment.json +++ b/packages/core/src/plugins/__tests__/consent/mockSettings/ConsentNotEnabledAtSegment.json @@ -68,4 +68,3 @@ "legacyVideoPluginsEnabled": false, "remotePlugins": [] } - diff --git a/packages/core/src/plugins/__tests__/consent/mockSettings/DestinationsMultipleCategories.json b/packages/core/src/plugins/__tests__/consent/mockSettings/DestinationsMultipleCategories.json index e7244523..f6f91f7a 100644 --- a/packages/core/src/plugins/__tests__/consent/mockSettings/DestinationsMultipleCategories.json +++ b/packages/core/src/plugins/__tests__/consent/mockSettings/DestinationsMultipleCategories.json @@ -64,10 +64,10 @@ "legacyVideoPluginsEnabled": false, "remotePlugins": [], "consentSettings": { + "hasUnmappedDestinations": false, "allCategories": [ "C0001", "C0002" ] } } - diff --git a/packages/core/src/plugins/__tests__/consent/mockSettings/NoUnmappedDestinations.json b/packages/core/src/plugins/__tests__/consent/mockSettings/NoUnmappedDestinations.json index 6d190cd4..42ecc011 100644 --- a/packages/core/src/plugins/__tests__/consent/mockSettings/NoUnmappedDestinations.json +++ b/packages/core/src/plugins/__tests__/consent/mockSettings/NoUnmappedDestinations.json @@ -93,6 +93,7 @@ "legacyVideoPluginsEnabled": false, "remotePlugins": [], "consentSettings": { + "hasUnmappedDestinations": false, "allCategories": [ "C0001", "C0002", @@ -102,4 +103,3 @@ ] } } - diff --git a/packages/core/src/plugins/__tests__/consent/mockSettings/UnmappedDestinations.json b/packages/core/src/plugins/__tests__/consent/mockSettings/UnmappedDestinations.json index 2e3a6dcf..b36ff21c 100644 --- a/packages/core/src/plugins/__tests__/consent/mockSettings/UnmappedDestinations.json +++ b/packages/core/src/plugins/__tests__/consent/mockSettings/UnmappedDestinations.json @@ -89,6 +89,7 @@ "legacyVideoPluginsEnabled": false, "remotePlugins": [], "consentSettings": { + "hasUnmappedDestinations": true, "allCategories": [ "C0001", "C0002", @@ -98,4 +99,3 @@ ] } } - diff --git a/packages/core/src/plugins/__tests__/consent/noUnmapped.test.ts b/packages/core/src/plugins/__tests__/consent/noUnmapped.test.ts index 17f86fb8..0c00a3a9 100644 --- a/packages/core/src/plugins/__tests__/consent/noUnmapped.test.ts +++ b/packages/core/src/plugins/__tests__/consent/noUnmapped.test.ts @@ -8,6 +8,7 @@ describe('No unmapped destinations', () => { const createClient = () => createTestClient({ settings: noUnmappedDestinations.integrations, + consentSettings: noUnmappedDestinations.consentSettings, }); test('no to all', async () => { diff --git a/packages/core/src/plugins/__tests__/consent/unmapped.test.ts b/packages/core/src/plugins/__tests__/consent/unmapped.test.ts index 1678e77a..3425333b 100644 --- a/packages/core/src/plugins/__tests__/consent/unmapped.test.ts +++ b/packages/core/src/plugins/__tests__/consent/unmapped.test.ts @@ -13,6 +13,7 @@ describe('Unmapped destinations', () => { createTestClient( { settings: unmappedDestinations.integrations, + consentSettings: unmappedDestinations.consentSettings, }, { autoAddSegmentDestination: true } ); diff --git a/packages/core/src/storage/sovranStorage.ts b/packages/core/src/storage/sovranStorage.ts index 2e0c144e..44594bee 100644 --- a/packages/core/src/storage/sovranStorage.ts +++ b/packages/core/src/storage/sovranStorage.ts @@ -14,6 +14,7 @@ import type { UserInfoState, RoutingRule, DestinationFilters, + SegmentAPIConsentSettings, } from '..'; import { getUUID } from '../uuid'; import { createGetter } from './helpers'; @@ -34,6 +35,7 @@ type Data = { eventsToRetry: SegmentEvent[]; context: DeepPartial; settings: SegmentAPIIntegrations; + consentSettings: SegmentAPIConsentSettings | undefined; userInfo: UserInfoState; filters: DestinationFilters; }; @@ -43,6 +45,7 @@ const INITIAL_VALUES: Data = { eventsToRetry: [], context: {}, settings: {}, + consentSettings: undefined, filters: {}, userInfo: { anonymousId: getUUID(), @@ -141,6 +144,9 @@ export class SovranStorage implements Storage { private storePersistorSaveDelay?: number; private readinessStore: Store; private contextStore: Store<{ context: DeepPartial }>; + private consentSettingsStore: Store<{ + consentSettings: SegmentAPIConsentSettings | undefined; + }>; private settingsStore: Store<{ settings: SegmentAPIIntegrations }>; private userInfoStore: Store<{ userInfo: UserInfoState }>; private deepLinkStore: Store = deepLinkStore; @@ -155,6 +161,9 @@ export class SovranStorage implements Storage { Settable & Dictionary; + readonly consentSettings: Watchable & + Settable; + readonly filters: Watchable & Settable & Dictionary; @@ -271,6 +280,44 @@ export class SovranStorage implements Storage { }, }; + // Consent settings + + this.consentSettingsStore = createStore( + { consentSettings: INITIAL_VALUES.consentSettings }, + { + persist: { + storeId: `${this.storeId}-consentSettings`, + persistor: this.storePersistor, + saveDelay: this.storePersistorSaveDelay, + onInitialized: markAsReadyGenerator('hasRestoredSettings'), + }, + } + ); + + this.consentSettings = { + get: createStoreGetter(this.consentSettingsStore, 'consentSettings'), + onChange: ( + callback: (value?: SegmentAPIConsentSettings | undefined) => void + ) => + this.consentSettingsStore.subscribe((store) => + callback(store.consentSettings) + ), + set: async (value) => { + const { consentSettings } = await this.consentSettingsStore.dispatch( + (state) => { + let newState: typeof state.consentSettings; + if (value instanceof Function) { + newState = value(state.consentSettings); + } else { + newState = Object.assign({}, state.consentSettings, value); + } + return { consentSettings: newState }; + } + ); + return consentSettings; + }, + }; + // Filters this.filtersStore = createStore(INITIAL_VALUES.filters, { diff --git a/packages/core/src/storage/types.ts b/packages/core/src/storage/types.ts index 02868f8c..a1baee17 100644 --- a/packages/core/src/storage/types.ts +++ b/packages/core/src/storage/types.ts @@ -1,4 +1,5 @@ import type { Unsubscribe, Persistor } from '@segment/sovran-react-native'; +import type { SegmentAPIConsentSettings } from '..'; import type { Context, DeepPartial, @@ -71,6 +72,9 @@ export interface Storage { Settable & Dictionary; + readonly consentSettings: Watchable & + Settable; + readonly filters: Watchable & Settable & Dictionary; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 709d0adc..141ca3c4 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -290,6 +290,11 @@ export type SegmentAPIIntegrations = { [key: string]: IntegrationSettings; }; +export type SegmentAPIConsentSettings = { + allCategories: string[]; + hasUnmappedDestinations: boolean; +}; + export type RoutingRule = Rule; export interface MetricsOptions { @@ -309,6 +314,7 @@ export type SegmentAPISettings = { routingRules: RoutingRule[]; }; metrics?: MetricsOptions; + consentSettings?: SegmentAPIConsentSettings; }; export type DestinationMetadata = {