Skip to content

Commit 545d596

Browse files
zikaarioscb
authored andcommitted
feat: add support for hasUnmappedDestinations (#905)
1 parent ff1d332 commit 545d596

13 files changed

+113
-10
lines changed

packages/core/src/__tests__/__helpers__/mockSegmentStore.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
DestinationFilters,
1313
IntegrationSettings,
1414
RoutingRule,
15+
SegmentAPIConsentSettings,
1516
SegmentAPIIntegrations,
1617
UserInfoState,
1718
} from '../../types';
@@ -22,6 +23,7 @@ export type StoreData = {
2223
isReady: boolean;
2324
context?: DeepPartial<Context>;
2425
settings: SegmentAPIIntegrations;
26+
consentSettings?: SegmentAPIConsentSettings;
2527
filters: DestinationFilters;
2628
userInfo: UserInfoState;
2729
deepLinkData: DeepLinkData;
@@ -33,6 +35,7 @@ const INITIAL_VALUES: StoreData = {
3335
settings: {
3436
[SEGMENT_DESTINATION_KEY]: {},
3537
},
38+
consentSettings: undefined,
3639
filters: {},
3740
userInfo: {
3841
anonymousId: 'anonymousId',
@@ -71,6 +74,9 @@ export class MockSegmentStore implements Storage {
7174
private callbacks = {
7275
context: createCallbackManager<DeepPartial<Context> | undefined>(),
7376
settings: createCallbackManager<SegmentAPIIntegrations>(),
77+
consentSettings: createCallbackManager<
78+
SegmentAPIConsentSettings | undefined
79+
>(),
7480
filters: createCallbackManager<DestinationFilters>(),
7581
userInfo: createCallbackManager<UserInfoState>(),
7682
deepLinkData: createCallbackManager<DeepLinkData>(),
@@ -123,6 +129,19 @@ export class MockSegmentStore implements Storage {
123129
},
124130
};
125131

132+
readonly consentSettings: Watchable<SegmentAPIConsentSettings | undefined> &
133+
Settable<SegmentAPIConsentSettings | undefined> = {
134+
get: createMockStoreGetter(() => this.data.consentSettings),
135+
onChange: (callback: (value?: SegmentAPIConsentSettings) => void) =>
136+
this.callbacks.consentSettings.register(callback),
137+
set: (value) => {
138+
this.data.consentSettings =
139+
value instanceof Function ? value(this.data.consentSettings) : value;
140+
this.callbacks.consentSettings.run(this.data.consentSettings);
141+
return this.data.consentSettings;
142+
},
143+
};
144+
126145
readonly filters: Watchable<DestinationFilters | undefined> &
127146
Settable<DestinationFilters> &
128147
Dictionary<string, RoutingRule, DestinationFilters> = {

packages/core/src/analytics.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
SegmentError,
6060
translateHTTPError,
6161
} from './errors';
62+
import type { SegmentAPIConsentSettings } from '.';
6263

6364
type OnPluginAddedCallback = (plugin: Plugin) => void;
6465

@@ -116,6 +117,11 @@ export class SegmentClient {
116117
*/
117118
readonly settings: Watchable<SegmentAPIIntegrations | undefined>;
118119

120+
/**
121+
* Access or subscribe to integration settings
122+
*/
123+
readonly consentSettings: Watchable<SegmentAPIConsentSettings | undefined>;
124+
119125
/**
120126
* Access or subscribe to destination filter settings
121127
*/
@@ -198,6 +204,11 @@ export class SegmentClient {
198204
onChange: this.store.settings.onChange,
199205
};
200206

207+
this.consentSettings = {
208+
get: this.store.consentSettings.get,
209+
onChange: this.store.consentSettings.onChange,
210+
};
211+
201212
this.filters = {
202213
get: this.store.filters.get,
203214
onChange: this.store.filters.onChange,
@@ -305,12 +316,14 @@ export class SegmentClient {
305316
const resJson: SegmentAPISettings =
306317
(await res.json()) as SegmentAPISettings;
307318
const integrations = resJson.integrations;
319+
const consentSettings = resJson.consentSettings;
308320
const filters = this.generateFiltersMap(
309321
resJson.middlewareSettings?.routingRules ?? []
310322
);
311323
this.logger.info(`Received settings from Segment succesfully.`);
312324
await Promise.all([
313325
this.store.settings.set(integrations),
326+
this.store.consentSettings.set(consentSettings),
314327
this.store.filters.set(filters),
315328
]);
316329
} catch (e) {

packages/core/src/plugins/ConsentPlugin.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,20 @@ export class ConsentPlugin extends Plugin {
8080
if (this.isDestinationPlugin(plugin)) {
8181
plugin.add(
8282
new ConsentFilterPlugin((event) => {
83+
const allCategories =
84+
this.analytics?.consentSettings.get()?.allCategories || [];
8385
const settings = this.analytics?.settings.get() || {};
8486
const preferences = event.context?.consent?.categoryPreferences || {};
8587

8688
if (plugin.key === SEGMENT_DESTINATION_KEY) {
89+
const noneConsented = allCategories.every(
90+
(category) => !preferences[category]
91+
);
92+
8793
return (
8894
this.isConsentUpdateEvent(event) ||
89-
!(
90-
Object.values(preferences).every((consented) => !consented) &&
91-
Object.entries(settings)
92-
.filter(([k]) => k !== SEGMENT_DESTINATION_KEY)
93-
.every(([_, v]) => this.containsConsentSettings(v))
94-
)
95+
!this.isConsentFeatureSetup() ||
96+
!(noneConsented && !this.hasUnmappedDestinations())
9597
);
9698
}
9799

@@ -127,6 +129,16 @@ export class ConsentPlugin extends Plugin {
127129
private isConsentUpdateEvent(event: SegmentEvent): boolean {
128130
return (event as TrackEventType).event === CONSENT_PREF_UPDATE_EVENT;
129131
}
132+
133+
private hasUnmappedDestinations(): boolean {
134+
return (
135+
this.analytics?.consentSettings.get()?.hasUnmappedDestinations === true
136+
);
137+
}
138+
139+
private isConsentFeatureSetup(): boolean {
140+
return typeof this.analytics?.consentSettings.get() === 'object';
141+
}
130142
}
131143

132144
/**

packages/core/src/plugins/__tests__/consent/destinationMultipleCategories.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('Destinations multiple categories', () => {
1313
createTestClient(
1414
{
1515
settings: destinationsMultipleCategories.integrations,
16+
consentSettings: destinationsMultipleCategories.consentSettings,
1617
},
1718
{ autoAddSegmentDestination: true }
1819
);

packages/core/src/plugins/__tests__/consent/mockSettings/ConsentNotEnabledAtSegment.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,3 @@
6868
"legacyVideoPluginsEnabled": false,
6969
"remotePlugins": []
7070
}
71-

packages/core/src/plugins/__tests__/consent/mockSettings/DestinationsMultipleCategories.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@
6464
"legacyVideoPluginsEnabled": false,
6565
"remotePlugins": [],
6666
"consentSettings": {
67+
"hasUnmappedDestinations": false,
6768
"allCategories": [
6869
"C0001",
6970
"C0002"
7071
]
7172
}
7273
}
73-

packages/core/src/plugins/__tests__/consent/mockSettings/NoUnmappedDestinations.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"legacyVideoPluginsEnabled": false,
9494
"remotePlugins": [],
9595
"consentSettings": {
96+
"hasUnmappedDestinations": false,
9697
"allCategories": [
9798
"C0001",
9899
"C0002",
@@ -102,4 +103,3 @@
102103
]
103104
}
104105
}
105-

packages/core/src/plugins/__tests__/consent/mockSettings/UnmappedDestinations.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"legacyVideoPluginsEnabled": false,
9090
"remotePlugins": [],
9191
"consentSettings": {
92+
"hasUnmappedDestinations": true,
9293
"allCategories": [
9394
"C0001",
9495
"C0002",
@@ -98,4 +99,3 @@
9899
]
99100
}
100101
}
101-

packages/core/src/plugins/__tests__/consent/noUnmapped.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ describe('No unmapped destinations', () => {
88
const createClient = () =>
99
createTestClient({
1010
settings: noUnmappedDestinations.integrations,
11+
consentSettings: noUnmappedDestinations.consentSettings,
1112
});
1213

1314
test('no to all', async () => {

packages/core/src/plugins/__tests__/consent/unmapped.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('Unmapped destinations', () => {
1313
createTestClient(
1414
{
1515
settings: unmappedDestinations.integrations,
16+
consentSettings: unmappedDestinations.consentSettings,
1617
},
1718
{ autoAddSegmentDestination: true }
1819
);

packages/core/src/storage/sovranStorage.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
UserInfoState,
1515
RoutingRule,
1616
DestinationFilters,
17+
SegmentAPIConsentSettings,
1718
} from '..';
1819
import { getUUID } from '../uuid';
1920
import { createGetter } from './helpers';
@@ -34,6 +35,7 @@ type Data = {
3435
eventsToRetry: SegmentEvent[];
3536
context: DeepPartial<Context>;
3637
settings: SegmentAPIIntegrations;
38+
consentSettings: SegmentAPIConsentSettings | undefined;
3739
userInfo: UserInfoState;
3840
filters: DestinationFilters;
3941
};
@@ -43,6 +45,7 @@ const INITIAL_VALUES: Data = {
4345
eventsToRetry: [],
4446
context: {},
4547
settings: {},
48+
consentSettings: undefined,
4649
filters: {},
4750
userInfo: {
4851
anonymousId: getUUID(),
@@ -141,6 +144,9 @@ export class SovranStorage implements Storage {
141144
private storePersistorSaveDelay?: number;
142145
private readinessStore: Store<ReadinessStore>;
143146
private contextStore: Store<{ context: DeepPartial<Context> }>;
147+
private consentSettingsStore: Store<{
148+
consentSettings: SegmentAPIConsentSettings | undefined;
149+
}>;
144150
private settingsStore: Store<{ settings: SegmentAPIIntegrations }>;
145151
private userInfoStore: Store<{ userInfo: UserInfoState }>;
146152
private deepLinkStore: Store<DeepLinkData> = deepLinkStore;
@@ -155,6 +161,9 @@ export class SovranStorage implements Storage {
155161
Settable<SegmentAPIIntegrations> &
156162
Dictionary<string, IntegrationSettings, SegmentAPIIntegrations>;
157163

164+
readonly consentSettings: Watchable<SegmentAPIConsentSettings | undefined> &
165+
Settable<SegmentAPIConsentSettings | undefined>;
166+
158167
readonly filters: Watchable<DestinationFilters | undefined> &
159168
Settable<DestinationFilters> &
160169
Dictionary<string, RoutingRule, DestinationFilters>;
@@ -271,6 +280,44 @@ export class SovranStorage implements Storage {
271280
},
272281
};
273282

283+
// Consent settings
284+
285+
this.consentSettingsStore = createStore(
286+
{ consentSettings: INITIAL_VALUES.consentSettings },
287+
{
288+
persist: {
289+
storeId: `${this.storeId}-consentSettings`,
290+
persistor: this.storePersistor,
291+
saveDelay: this.storePersistorSaveDelay,
292+
onInitialized: markAsReadyGenerator('hasRestoredSettings'),
293+
},
294+
}
295+
);
296+
297+
this.consentSettings = {
298+
get: createStoreGetter(this.consentSettingsStore, 'consentSettings'),
299+
onChange: (
300+
callback: (value?: SegmentAPIConsentSettings | undefined) => void
301+
) =>
302+
this.consentSettingsStore.subscribe((store) =>
303+
callback(store.consentSettings)
304+
),
305+
set: async (value) => {
306+
const { consentSettings } = await this.consentSettingsStore.dispatch(
307+
(state) => {
308+
let newState: typeof state.consentSettings;
309+
if (value instanceof Function) {
310+
newState = value(state.consentSettings);
311+
} else {
312+
newState = Object.assign({}, state.consentSettings, value);
313+
}
314+
return { consentSettings: newState };
315+
}
316+
);
317+
return consentSettings;
318+
},
319+
};
320+
274321
// Filters
275322

276323
this.filtersStore = createStore(INITIAL_VALUES.filters, {

packages/core/src/storage/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Unsubscribe, Persistor } from '@segment/sovran-react-native';
2+
import type { SegmentAPIConsentSettings } from '..';
23
import type {
34
Context,
45
DeepPartial,
@@ -71,6 +72,9 @@ export interface Storage {
7172
Settable<SegmentAPIIntegrations> &
7273
Dictionary<string, IntegrationSettings, SegmentAPIIntegrations>;
7374

75+
readonly consentSettings: Watchable<SegmentAPIConsentSettings | undefined> &
76+
Settable<SegmentAPIConsentSettings | undefined>;
77+
7478
readonly filters: Watchable<DestinationFilters | undefined> &
7579
Settable<DestinationFilters> &
7680
Dictionary<string, RoutingRule, DestinationFilters>;

packages/core/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,11 @@ export type SegmentAPIIntegrations = {
290290
[key: string]: IntegrationSettings;
291291
};
292292

293+
export type SegmentAPIConsentSettings = {
294+
allCategories: string[];
295+
hasUnmappedDestinations: boolean;
296+
};
297+
293298
export type RoutingRule = Rule;
294299

295300
export interface MetricsOptions {
@@ -309,6 +314,7 @@ export type SegmentAPISettings = {
309314
routingRules: RoutingRule[];
310315
};
311316
metrics?: MetricsOptions;
317+
consentSettings?: SegmentAPIConsentSettings;
312318
};
313319

314320
export type DestinationMetadata = {

0 commit comments

Comments
 (0)