diff --git a/packages/core/src/plugins/ConsentPlugin.ts b/packages/core/src/plugins/ConsentPlugin.ts index 1651b5f7..3846f73c 100644 --- a/packages/core/src/plugins/ConsentPlugin.ts +++ b/packages/core/src/plugins/ConsentPlugin.ts @@ -47,24 +47,25 @@ export class ConsentPlugin extends Plugin { analytics.getPlugins().forEach(this.injectConsentFilterIfApplicable); analytics.onPluginLoaded(this.injectConsentFilterIfApplicable); this.consentCategoryProvider.setApplicableCategories(this.categories); - this.consentCategoryProvider.onConsentChange((categoryPreferences) => { - this.analytics - ?.track(CONSENT_PREF_UPDATE_EVENT, { - consent: { - categoryPreferences, - }, - }) - .catch((e) => { - throw e; - }); + this.consentCategoryProvider.onConsentChange(() => { + this.notifyConsentChange(); + }); + + let lastDeviceAttrs = analytics.context.get()?.device; + analytics.context.onChange((c) => { + const newAttrs = c?.device; + if ( + newAttrs?.adTrackingEnabled !== lastDeviceAttrs?.adTrackingEnabled || + newAttrs?.advertisingId !== lastDeviceAttrs?.advertisingId || + newAttrs?.trackingStatus !== lastDeviceAttrs?.trackingStatus + ) { + this.notifyConsentChange(); + } + lastDeviceAttrs = newAttrs; }); } async execute(event: SegmentEvent): Promise { - if (this.isConsentUpdateEvent(event)) { - return event; - } - event.context = { ...event.context, consent: { @@ -143,6 +144,13 @@ export class ConsentPlugin extends Plugin { private isConsentFeatureSetup(): boolean { return typeof this.analytics?.consentSettings.get() === 'object'; } + + private notifyConsentChange() { + // actual preferences will be attached in the execute method + this.analytics?.track(CONSENT_PREF_UPDATE_EVENT).catch((e) => { + throw e; + }); + } } /** diff --git a/packages/core/src/plugins/__tests__/consent/idfa.test.ts b/packages/core/src/plugins/__tests__/consent/idfa.test.ts new file mode 100644 index 00000000..fa40aae0 --- /dev/null +++ b/packages/core/src/plugins/__tests__/consent/idfa.test.ts @@ -0,0 +1,95 @@ +import { IdfaPlugin } from '@segment/analytics-react-native-plugin-idfa'; +import { createTestClient } from '../../../test-helpers'; +import { ConsentPlugin } from '../../ConsentPlugin'; +import { createConsentProvider } from './utils'; +import noUnmappedDestinations from './mockSettings/NoUnmappedDestinations.json'; +import type { Context, ContextDevice } from '@segment/analytics-react-native'; + +let mockIdfaValue = { + adTrackingEnabled: false, + advertisingId: 'trackMeId', + trackingStatus: 'denied', +}; + +jest.mock( + '@segment/analytics-react-native-plugin-idfa/lib/commonjs/AnalyticsReactNativePluginIdfa', + () => ({ + AnalyticsReactNativePluginIdfa: { + getTrackingAuthorizationStatus: async () => { + return Promise.resolve(mockIdfaValue); + }, + }, + }) +); + +describe('IDFA x Consent', () => { + it('triggers consent update event on IDFA change and includes IDFA data', async () => { + const { client, expectEvent } = createTestClient( + { + settings: noUnmappedDestinations.integrations, + consentSettings: noUnmappedDestinations.consentSettings, + }, + { autoAddSegmentDestination: true } + ); + + const mockConsentStatuses = { + C0001: false, + C0002: false, + C0003: false, + C0004: false, + C0005: false, + }; + + client.add({ + plugin: new ConsentPlugin( + createConsentProvider(mockConsentStatuses), + Object.keys(mockConsentStatuses) + ), + }); + + const idfaPlugin = new IdfaPlugin(false); + client.add({ + plugin: idfaPlugin, + }); + + await client.init(); + + await idfaPlugin.requestTrackingPermission(); + + await new Promise((r) => setTimeout(r, 1000)); + + expectEvent({ + event: 'Segment Consent Preference', + context: expect.objectContaining({ + device: expect.objectContaining({ + adTrackingEnabled: false, + advertisingId: 'trackMeId', + trackingStatus: 'denied', + }) as unknown as ContextDevice, + }) as unknown as Context, + }); + + // update IDFA data + + mockIdfaValue = { + adTrackingEnabled: true, + advertisingId: 'trackMeId', + trackingStatus: 'authorized', + }; + + await idfaPlugin.requestTrackingPermission(); + + await new Promise((r) => setTimeout(r, 1000)); + + expectEvent({ + event: 'Segment Consent Preference', + context: expect.objectContaining({ + device: expect.objectContaining({ + adTrackingEnabled: true, + advertisingId: 'trackMeId', + trackingStatus: 'authorized', + }) as unknown as ContextDevice, + }) as unknown as Context, + }); + }); +}); diff --git a/packages/plugins/plugin-onetrust/src/__tests__/OneTrust.test.ts b/packages/plugins/plugin-onetrust/src/__tests__/OneTrust.test.ts index 136d80de..d5e342a3 100644 --- a/packages/plugins/plugin-onetrust/src/__tests__/OneTrust.test.ts +++ b/packages/plugins/plugin-onetrust/src/__tests__/OneTrust.test.ts @@ -1,8 +1,10 @@ import { + Context, DestinationPlugin, Plugin, PluginType, SegmentClient, + SegmentEvent, } from '@segment/analytics-react-native'; import { createTestClient } from '@segment/analytics-react-native/src/test-helpers'; import onChange from 'on-change'; @@ -63,6 +65,7 @@ class MockOneTrustSDK implements OTPublishersNativeSDK { describe('OneTrustPlugin', () => { let client: SegmentClient; + let expectEvent: (event: Partial) => void; let mockOneTrust: MockOneTrustSDK; const mockBraze = new MockDestination('Braze'); const mockAmplitude = new MockDestination('Amplitude'); @@ -72,6 +75,7 @@ describe('OneTrustPlugin', () => { testClient.store.reset(); jest.clearAllMocks(); client = testClient.client as unknown as SegmentClient; + expectEvent = testClient.expectEvent; mockOneTrust = new MockOneTrustSDK(); client.add({ plugin: new OneTrustPlugin( @@ -176,15 +180,20 @@ describe('OneTrustPlugin', () => { // this is to make sure there are no unneccessary Consent Preference track calls expect(spy).toHaveBeenCalledTimes(2); - expect(spy).toHaveBeenLastCalledWith('Segment Consent Preference', { - consent: { - categoryPreferences: { - C001: true, - C002: true, - C003: true, - C004: false, + expect(spy).toHaveBeenLastCalledWith('Segment Consent Preference'); + + expectEvent({ + event: 'Segment Consent Preference', + context: expect.objectContaining({ + consent: { + categoryPreferences: { + C001: true, + C002: true, + C003: true, + C004: false, + }, }, - }, + }) as unknown as Context, }); }); });