Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 87 additions & 12 deletions src/integrationCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getCookies,
getHref,
isEmpty,
valueof,
} from './utils';

export interface IntegrationCaptureProcessorFunction {
Expand Down Expand Up @@ -48,32 +49,76 @@ 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<typeof IntegrationOutputs>;
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,
},
};

export default class IntegrationCapture {
public clickIds: Dictionary<string>;
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);
}

/**
Expand Down Expand Up @@ -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<string>} The partner identities.
*/
public getClickIdsAsPartnerIdentities(): Dictionary<string> {
return this.getClickIds(this.clickIds, this.filteredPartnerIdentityMappings);
}

if (!this.clickIds) {
return customFlags;
private getClickIds(
clickIds: Dictionary<string>,
mappingList: IntegrationIdMapping
): Dictionary<string> {
const mappedClickIds: Dictionary<string> = {};

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<string>, url?: string, timestamp?: number): Dictionary<string> {
private applyProcessors(
clickIds: Dictionary<string>,
url?: string,
timestamp?: number
): Dictionary<string> {
const processedClickIds: Dictionary<string> = {};

for (const [key, value] of Object.entries(clickIds)) {
Expand All @@ -150,4 +215,14 @@ export default class IntegrationCapture {

return processedClickIds;
}

private filterMappings(
outputType: valueof<typeof IntegrationOutputs>
): IntegrationIdMapping {
return Object.fromEntries(
Object.entries(integrationMapping).filter(
([, value]) => value.output === outputType
)
);
}
}
21 changes: 18 additions & 3 deletions src/sdkToEventsApiConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;

// 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;
}
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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;
}

Expand Down
90 changes: 89 additions & 1 deletion test/jest/integration-capture.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -65,6 +85,7 @@ describe('Integration Capture', () => {
});
});

describe('Facebook Click Ids', () => {
it('should format fbclid correctly', () => {
jest.spyOn(Date, 'now').mockImplementation(() => 42);

Expand Down Expand Up @@ -147,7 +168,7 @@ describe('Integration Capture', () => {
fbclid: 'fb.2.42.12345',
});
});

});
});

describe('#captureQueryParams', () => {
Expand Down Expand Up @@ -184,6 +205,8 @@ describe('Integration Capture', () => {

expect(clickIds).toEqual({
fbclid: 'fb.2.42.67890',
ttclid: '12345',
gclid: '54321',
});
});

Expand Down Expand Up @@ -253,19 +276,63 @@ 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();

expect(customFlags).toEqual({
'Facebook.ClickId': '67890',
'Facebook.BrowserId': '54321',
'TikTok.ClickId': '12345',
'GoogleEnhancedConversions.Gclid': '123233.23131',
});
});

Expand All @@ -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', () => {
Expand Down
Loading