diff --git a/src/integrationCapture.ts b/src/integrationCapture.ts index 7799b6e36..89228e0d1 100644 --- a/src/integrationCapture.ts +++ b/src/integrationCapture.ts @@ -5,6 +5,7 @@ import { getCookies, getHref, isEmpty, + valueof, } from './utils'; export interface IntegrationCaptureProcessorFunction { @@ -48,23 +49,60 @@ export const facebookClickIdProcessor: IntegrationCaptureProcessorFunction = ( return `fb.${subdomainIndex}.${_timestamp}.${clickId}`; }; + +const IntegrationOutputs = { + CUSTOM_FLAGS: 'custom_flags', + PARTNER_IDENTITIES: 'partner_identities', +} as const; + +interface IntegrationMappingItem { + mappedKey: string; + output: valueof; + processor?: IntegrationCaptureProcessorFunction; +} + interface IntegrationIdMapping { - [key: string]: { - mappedKey: string; - processor?: IntegrationCaptureProcessorFunction; - }; + [key: string]: IntegrationMappingItem } const integrationMapping: IntegrationIdMapping = { + // Facebook / Meta fbclid: { mappedKey: 'Facebook.ClickId', processor: facebookClickIdProcessor, + output: IntegrationOutputs.CUSTOM_FLAGS, }, _fbp: { mappedKey: 'Facebook.BrowserId', + output: IntegrationOutputs.CUSTOM_FLAGS, }, _fbc: { mappedKey: 'Facebook.ClickId', + output: IntegrationOutputs.CUSTOM_FLAGS, + }, + + // GOOGLE + gclid: { + mappedKey: 'GoogleEnhancedConversions.Gclid', + output: IntegrationOutputs.CUSTOM_FLAGS, + }, + gbraid: { + mappedKey: 'GoogleEnhancedConversions.Gbraid', + output: IntegrationOutputs.CUSTOM_FLAGS, + }, + wbraid: { + mappedKey: 'GoogleEnhancedConversions.Wbraid', + output: IntegrationOutputs.CUSTOM_FLAGS, + }, + + // TIK TOK + _ttp: { + mappedKey: 'tiktok_cookie_id', + output: IntegrationOutputs.PARTNER_IDENTITIES, + }, + ttclid: { + mappedKey: 'TikTok.ClickId', + output: IntegrationOutputs.CUSTOM_FLAGS, }, }; @@ -72,8 +110,15 @@ export default class IntegrationCapture { public clickIds: Dictionary; public readonly initialTimestamp: number; + public readonly filteredPartnerIdentityMappings: IntegrationIdMapping; + public readonly filteredCustomFlagMappings: IntegrationIdMapping; + constructor() { this.initialTimestamp = Date.now(); + + // Cache filtered mappings for faster access + this.filteredPartnerIdentityMappings = this.filterMappings(IntegrationOutputs.PARTNER_IDENTITIES); + this.filteredCustomFlagMappings = this.filterMappings(IntegrationOutputs.CUSTOM_FLAGS); } /** @@ -121,22 +166,42 @@ export default class IntegrationCapture { * @returns {SDKEventCustomFlags} The custom flags. */ public getClickIdsAsCustomFlags(): SDKEventCustomFlags { - const customFlags: SDKEventCustomFlags = {}; + return this.getClickIds(this.clickIds, this.filteredCustomFlagMappings); + } + + /** + * Converts the captured click IDs to partner identities. + * @returns {Dictionary} The partner identities. + */ + public getClickIdsAsPartnerIdentities(): Dictionary { + return this.getClickIds(this.clickIds, this.filteredPartnerIdentityMappings); + } - if (!this.clickIds) { - return customFlags; + private getClickIds( + clickIds: Dictionary, + mappingList: IntegrationIdMapping + ): Dictionary { + const mappedClickIds: Dictionary = {}; + + if (!clickIds) { + return mappedClickIds; } - for (const [key, value] of Object.entries(this.clickIds)) { - const mappedKey = integrationMapping[key]?.mappedKey; + for (const [key, value] of Object.entries(clickIds)) { + const mappedKey = mappingList[key]?.mappedKey; if (!isEmpty(mappedKey)) { - customFlags[mappedKey] = value; + mappedClickIds[mappedKey] = value; } } - return customFlags; + + return mappedClickIds; } - private applyProcessors(clickIds: Dictionary, url?: string, timestamp?: number): Dictionary { + private applyProcessors( + clickIds: Dictionary, + url?: string, + timestamp?: number + ): Dictionary { const processedClickIds: Dictionary = {}; for (const [key, value] of Object.entries(clickIds)) { @@ -150,4 +215,14 @@ export default class IntegrationCapture { return processedClickIds; } + + private filterMappings( + outputType: valueof + ): IntegrationIdMapping { + return Object.fromEntries( + Object.entries(integrationMapping).filter( + ([, value]) => value.output === outputType + ) + ); + } } diff --git a/src/sdkToEventsApiConverter.ts b/src/sdkToEventsApiConverter.ts index f4c51b310..c3a3e314d 100644 --- a/src/sdkToEventsApiConverter.ts +++ b/src/sdkToEventsApiConverter.ts @@ -13,15 +13,23 @@ import { SDKCCPAConsentState, } from './consent'; import Types from './types'; -import { isEmpty } from './utils'; +import { Dictionary, isEmpty } from './utils'; import { ISDKUserIdentity } from './identity-user-interfaces'; import { SDKIdentityTypeEnum } from './identity.interfaces'; +type PartnerIdentities = Dictionary; + +// FIXME: Event Models version of Batch references `partner_identity` as a string +// when it should be a dictionary of strings called `partner_identities` +interface Batch extends EventsApi.Batch { + partner_identities?: PartnerIdentities; +} + export function convertEvents( mpid: string, sdkEvents: SDKEvent[], mpInstance: MParticleWebSDK -): EventsApi.Batch | null { +): Batch | null { if (!mpid) { return null; } @@ -56,7 +64,7 @@ export function convertEvents( currentConsentState = user.getConsentState(); } - const upload: EventsApi.Batch = { + const upload: Batch = { source_request_id: mpInstance._Helpers.generateUniqueId(), mpid, timestamp_unixtime_ms: new Date().getTime(), @@ -102,6 +110,13 @@ export function convertEvents( }, }; } + + // FIXME: Should we store this on the Store? + const capturedPartnerIdentities: PartnerIdentities = mpInstance?._IntegrationCapture.getClickIdsAsPartnerIdentities(); + if (!isEmpty(capturedPartnerIdentities)) { + upload.partner_identities = capturedPartnerIdentities; + } + return upload; } diff --git a/test/jest/integration-capture.spec.ts b/test/jest/integration-capture.spec.ts index cef4f9724..981fec3ab 100644 --- a/test/jest/integration-capture.spec.ts +++ b/test/jest/integration-capture.spec.ts @@ -9,6 +9,26 @@ describe('Integration Capture', () => { const integrationCapture = new IntegrationCapture(); expect(integrationCapture.clickIds).toBeUndefined(); }); + + it('should initialize with a filtered list of partner identity mappings', () => { + const integrationCapture = new IntegrationCapture(); + const mappings = integrationCapture.filteredPartnerIdentityMappings; + expect(Object.keys(mappings)).toEqual(['_ttp']); + }); + + it('should initialize with a filtered list of custom flag mappings', () => { + const integrationCapture = new IntegrationCapture(); + const mappings = integrationCapture.filteredCustomFlagMappings; + expect(Object.keys(mappings)).toEqual([ + 'fbclid', + '_fbp', + '_fbc', + 'gclid', + 'gbraid', + 'wbraid', + 'ttclid', + ]); + }); }); describe('#capture', () => { @@ -65,6 +85,7 @@ describe('Integration Capture', () => { }); }); + describe('Facebook Click Ids', () => { it('should format fbclid correctly', () => { jest.spyOn(Date, 'now').mockImplementation(() => 42); @@ -147,7 +168,7 @@ describe('Integration Capture', () => { fbclid: 'fb.2.42.12345', }); }); - + }); }); describe('#captureQueryParams', () => { @@ -184,6 +205,8 @@ describe('Integration Capture', () => { expect(clickIds).toEqual({ fbclid: 'fb.2.42.67890', + ttclid: '12345', + gclid: '54321', }); }); @@ -253,12 +276,54 @@ describe('Integration Capture', () => { }); }); + describe('#getClickIdsAsPartnerIdentites', () => { + it('should return clickIds as partner identities', () => { + const integrationCapture = new IntegrationCapture(); + integrationCapture.clickIds = { + _ttp: '1234123999.123123', + }; + + const partnerIdentities = integrationCapture.getClickIdsAsPartnerIdentities(); + + expect(partnerIdentities).toEqual({ + tiktok_cookie_id: '1234123999.123123', + }); + }); + + it('should return empty object if clickIds is empty or undefined', () => { + const integrationCapture = new IntegrationCapture(); + const partnerIdentities = integrationCapture.getClickIdsAsPartnerIdentities(); + + expect(partnerIdentities).toEqual({}); + }); + + it.only('should only return mapped clickIds as partner identities', () => { + const integrationCapture = new IntegrationCapture(); + integrationCapture.clickIds = { + fbclid: '67890', + _fbp: '54321', + ttclid: '12345', + _ttp: '1234123999.123123', + gclid: '123233.23131', + invalidId: '12345', + }; + + const partnerIdentities = integrationCapture.getClickIdsAsPartnerIdentities(); + + expect(partnerIdentities).toEqual({ + tiktok_cookie_id: '1234123999.123123', + }); + }); + }); + describe('#getClickIdsAsCustomFlags', () => { it('should return clickIds as custom flags', () => { const integrationCapture = new IntegrationCapture(); integrationCapture.clickIds = { fbclid: '67890', _fbp: '54321', + ttclid: '12345', + gclid: '123233.23131', }; const customFlags = integrationCapture.getClickIdsAsCustomFlags(); @@ -266,6 +331,8 @@ describe('Integration Capture', () => { expect(customFlags).toEqual({ 'Facebook.ClickId': '67890', 'Facebook.BrowserId': '54321', + 'TikTok.ClickId': '12345', + 'GoogleEnhancedConversions.Gclid': '123233.23131', }); }); @@ -275,6 +342,27 @@ describe('Integration Capture', () => { expect(customFlags).toEqual({}); }); + + it('should only return mapped clickIds as custom flags', () => { + const integrationCapture = new IntegrationCapture(); + integrationCapture.clickIds = { + fbclid: '67890', + _fbp: '54321', + _ttp: '0823422223.23234', + ttclid: '12345', + gclid: '123233.23131', + invalidId: '12345', + }; + + const customFlags = integrationCapture.getClickIdsAsCustomFlags(); + + expect(customFlags).toEqual({ + 'Facebook.ClickId': '67890', + 'Facebook.BrowserId': '54321', + 'TikTok.ClickId': '12345', + 'GoogleEnhancedConversions.Gclid': '123233.23131', + }); + }); }); describe('#facebookClickIdProcessor', () => {