Skip to content
102 changes: 90 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,79 @@ export const facebookClickIdProcessor: IntegrationCaptureProcessorFunction = (

return `fb.${subdomainIndex}.${_timestamp}.${clickId}`;
};

// Integration outputs are used to determine how click ids are used within the SDK
// CUSTOM_FLAGS are sent out when an Event is created via ServerModel.createEventObject
// PARTNER_IDENTITIES are sent out in a Batch when a group of events are converted to a Batch

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,
},

// TIKTOK
ttclid: {
mappedKey: 'TikTok.Callback',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
_ttp: {
mappedKey: 'tiktok_cookie_id',
output: IntegrationOutputs.PARTNER_IDENTITIES,
},
};

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 +169,42 @@ export default class IntegrationCapture {
* @returns {SDKEventCustomFlags} The custom flags.
*/
public getClickIdsAsCustomFlags(): SDKEventCustomFlags {
const customFlags: SDKEventCustomFlags = {};
return this.getClickIds(this.clickIds, this.filteredCustomFlagMappings);
}

/**
* Returns only the `partner_identities` mapped integration output.
* @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 +218,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
)
);
}
}
2 changes: 1 addition & 1 deletion src/mp-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ export default function mParticleInstance(instanceName) {
Constants.NativeSdkPaths.Upload
);
} else {
self._APIClient.uploader.prepareAndUpload(false, false);
self._APIClient?.uploader?.prepareAndUpload(false, false);
}
}
};
Expand Down
40 changes: 37 additions & 3 deletions src/sdkToEventsApiConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,47 @@ 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';
import Constants from './constants';

const {
FeatureFlags
} = Constants;
const {
CaptureIntegrationSpecificIds
} = FeatureFlags;

type PartnerIdentities = Dictionary<string>;

// https://go.mparticle.com/work/SQDSDKS-6964
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;
}
if (!sdkEvents || sdkEvents.length < 1) {
return null;
}

const {
_IntegrationCapture,
_Helpers,
} = mpInstance

const {
getFeatureFlag,
} = _Helpers;


const user = mpInstance.Identity.getCurrentUser();

const uploadEvents: EventsApi.BaseEvent[] = [];
Expand Down Expand Up @@ -56,7 +81,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 +127,15 @@ export function convertEvents(
},
};
}

const isIntegrationCaptureEnabled: boolean = getFeatureFlag && Boolean(getFeatureFlag(CaptureIntegrationSpecificIds));
if (isIntegrationCaptureEnabled) {
const capturedPartnerIdentities: PartnerIdentities = _IntegrationCapture?.getClickIdsAsPartnerIdentities();
if (!isEmpty(capturedPartnerIdentities)) {
upload.partner_identities = capturedPartnerIdentities;
}
}

return upload;
}

Expand Down
86 changes: 82 additions & 4 deletions 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 @@ -46,7 +66,7 @@ describe('Integration Capture', () => {
it('should pass all clickIds to clickIds object', () => {
jest.spyOn(Date, 'now').mockImplementation(() => 42);

const url = new URL('https://www.example.com/?fbclid=12345&');
const url = new URL('https://www.example.com/?fbclid=12345&gclid=54321&gbraid=67890&wbraid=09876');

window.document.cookie = '_cookie1=1234';
window.document.cookie = '_cookie2=39895811.9165333198';
Expand All @@ -62,9 +82,31 @@ describe('Integration Capture', () => {
expect(integrationCapture.clickIds).toEqual({
fbclid: 'fb.2.42.12345',
_fbp: '54321',
gclid: '54321',
gbraid: '67890',
wbraid: '09876',
});
});

describe('Google Click Ids', () => {
it('should capture Google specific click ids', () => {
const url = new URL('https://www.example.com/?gclid=54321&gbraid=67890&wbraid=09876');

window.location.href = url.href;
window.location.search = url.search;

const integrationCapture = new IntegrationCapture();
integrationCapture.capture();

expect(integrationCapture.clickIds).toEqual({
gclid: '54321',
gbraid: '67890',
wbraid: '09876',
});
});
});

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

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

});

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

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

Expand Down Expand Up @@ -254,26 +299,59 @@ describe('Integration Capture', () => {
});

describe('#getClickIdsAsCustomFlags', () => {
it('should return clickIds as custom flags', () => {
it('should return empty object if clickIds is empty or undefined', () => {
const integrationCapture = new IntegrationCapture();
const customFlags = integrationCapture.getClickIdsAsCustomFlags();

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.Callback': '12345',
'GoogleEnhancedConversions.Gclid': '123233.23131',
});
});
});

describe('#getClickIdsAsPartnerIdentites', () => {
it('should return empty object if clickIds is empty or undefined', () => {
const integrationCapture = new IntegrationCapture();
const customFlags = integrationCapture.getClickIdsAsCustomFlags();
const partnerIdentities = integrationCapture.getClickIdsAsPartnerIdentities();

expect(customFlags).toEqual({});
expect(partnerIdentities).toEqual({});
});

it('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',
});
});
});

Expand Down
Loading
Loading