Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for TikTok Click and Cookie Ids #956

Open
wants to merge 6 commits into
base: feat/click-ids
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
89 changes: 77 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,45 +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 Events is created via ServerModel.createEventObject
alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
// 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.ClickId',
alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -134,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);
}

if (!this.clickIds) {
return customFlags;
/**
* Converts the captured click IDs to partner identities.
alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
* @returns {Dictionary<string>} The partner identities.
*/
public getClickIdsAsPartnerIdentities(): Dictionary<string> {
return this.getClickIds(this.clickIds, this.filteredPartnerIdentityMappings);
}

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 @@ -163,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
)
);
}
}
41 changes: 38 additions & 3 deletions src/sdkToEventsApiConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,48 @@ 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>;


alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
// https://go.mparticle.com/work/SQDSDKS-6964
alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
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 +82,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 +128,15 @@ export function convertEvents(
},
};
}

const isIntegrationCaptureEnabled = getFeatureFlag && getFeatureFlag(CaptureIntegrationSpecificIds)
alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
if (isIntegrationCaptureEnabled) {
const capturedPartnerIdentities: PartnerIdentities = _IntegrationCapture?.getClickIdsAsPartnerIdentities();
if (!isEmpty(capturedPartnerIdentities)) {
upload.partner_identities = capturedPartnerIdentities;
}
}

return upload;
}

Expand Down
84 changes: 84 additions & 0 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 @@ -208,6 +228,7 @@ describe('Integration Capture', () => {
expect(clickIds).toEqual({
fbclid: 'fb.2.42.67890',
gclid: '54321',
ttclid: '12345',
alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
});
});

Expand Down Expand Up @@ -283,6 +304,7 @@ describe('Integration Capture', () => {
integrationCapture.clickIds = {
fbclid: '67890',
_fbp: '54321',
ttclid: '12345',
gclid: '123233.23131',
};

Expand All @@ -291,6 +313,7 @@ describe('Integration Capture', () => {
expect(customFlags).toEqual({
'Facebook.ClickId': '67890',
'Facebook.BrowserId': '54321',
'TikTok.ClickId': '12345',
'GoogleEnhancedConversions.Gclid': '123233.23131',
});
});
Expand All @@ -301,6 +324,67 @@ 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('#getClickIdsAsPartnerIdentites', () => {
it('should return clickIds as partner identities', () => {
const integrationCapture = new IntegrationCapture();
integrationCapture.clickIds = {
_ttp: '1234123999.123123',
alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
};

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', () => {
alexs-mparticle marked this conversation as resolved.
Show resolved Hide resolved
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('#facebookClickIdProcessor', () => {
Expand Down
Loading
Loading