Skip to content

Commit 7c59fc5

Browse files
authored
fix: prevent cached commerce attributes in Rokt placements (#100)
1 parent 1535005 commit 7c59fc5

3 files changed

Lines changed: 251 additions & 5 deletions

File tree

src/Rokt-Kit.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import type { IUserIdentities } from '@mparticle/web-sdk';
2121

2222
// BaseEvent not re-exported from @mparticle/web-sdk/internal, so we import directly from @mparticle/event-models.
2323
import { BaseEvent, CommerceEvent } from '@mparticle/event-models';
24+
import {
25+
isSelectPlacementsAttributePersistenceDenied,
26+
removeSelectPlacementsAttributePersistenceDeniedAttributes,
27+
} from './selectPlacementsAttributePersistence';
2428

2529
interface RoktKitSettings {
2630
accountId: string;
@@ -1091,7 +1095,7 @@ class RoktKit implements KitInterface {
10911095
): string {
10921096
const kitSettings = settings as unknown as RoktKitSettings;
10931097
const accountId = kitSettings.accountId;
1094-
this.userAttributes = filteredUserAttributes || {};
1098+
this.userAttributes = removeSelectPlacementsAttributePersistenceDeniedAttributes(filteredUserAttributes);
10951099
this._onboardingExpProvider = kitSettings.onboardingExpProvider;
10961100

10971101
const placementEventMapping = parseSettingsString<PlacementEventMappingEntry>(kitSettings.placementEventMapping);
@@ -1245,7 +1249,9 @@ class RoktKit implements KitInterface {
12451249
}
12461250

12471251
public setUserAttribute(key: string, value: unknown): string {
1248-
this.userAttributes[key] = value;
1252+
if (!isSelectPlacementsAttributePersistenceDenied(key)) {
1253+
this.userAttributes[key] = value;
1254+
}
12491255
return 'Successfully set user attribute for forwarder: ' + name;
12501256
}
12511257

@@ -1256,7 +1262,7 @@ class RoktKit implements KitInterface {
12561262

12571263
private handleIdentityComplete(user: IMParticleUser, eventType: RoktIdentityEventType, callbackName: string): string {
12581264
const filteredUser = user as FilteredUser;
1259-
this.userAttributes = user.getAllUserAttributes();
1265+
this.userAttributes = removeSelectPlacementsAttributePersistenceDeniedAttributes(user.getAllUserAttributes());
12601266
this.pendingIdentityEvents.push(this.buildIdentityEvent(eventType, filteredUser));
12611267
return 'Successfully called ' + callbackName + ' for forwarder: ' + name;
12621268
}
@@ -1402,7 +1408,8 @@ class RoktKit implements KitInterface {
14021408

14031409
private _dispatchPlacements(options: Record<string, unknown>): RoktSelection | Promise<RoktSelection> | undefined {
14041410
const attributes = ((options && (options.attributes as Record<string, unknown>)) || {}) as Record<string, unknown>;
1405-
const placementAttributes: Record<string, unknown> = { ...this.userAttributes, ...attributes };
1411+
const cachedUserAttributes = removeSelectPlacementsAttributePersistenceDeniedAttributes(this.userAttributes);
1412+
const placementAttributes: Record<string, unknown> = { ...cachedUserAttributes, ...attributes };
14061413

14071414
const filters = this.filters || {};
14081415
const userAttributeFilters = (filters.userAttributeFilters as string[]) || [];
@@ -1420,7 +1427,7 @@ class RoktKit implements KitInterface {
14201427
filteredAttributes = placementAttributes;
14211428
}
14221429

1423-
this.userAttributes = filteredAttributes;
1430+
this.userAttributes = removeSelectPlacementsAttributePersistenceDeniedAttributes(filteredAttributes);
14241431

14251432
const optimizelyAttributes = this._onboardingExpProvider === 'Optimizely' ? this.fetchOptimizely() : {};
14261433

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const SELECT_PLACEMENTS_ATTRIBUTE_PERSISTENCE_DENY_LIST = [
2+
'billingaddress1',
3+
'billingaddress2',
4+
'billingcity',
5+
'billingstate',
6+
'billingzipcode',
7+
'cartitems',
8+
'ccbin',
9+
'confirmationref',
10+
'conversiontype',
11+
'country',
12+
'couponcode',
13+
'currency',
14+
'language',
15+
'paymentserviceprovider',
16+
'paymentserviceproviderattribute',
17+
'paymenttype',
18+
'shippingaddress1',
19+
'shippingcity',
20+
'shippingcountry',
21+
'shippingmethod',
22+
'shippingstate',
23+
'shippingzipcode',
24+
'totalprice',
25+
];
26+
const SELECT_PLACEMENTS_ATTRIBUTE_PERSISTENCE_DENY_SET = new Set(SELECT_PLACEMENTS_ATTRIBUTE_PERSISTENCE_DENY_LIST);
27+
28+
export function isSelectPlacementsAttributePersistenceDenied(key: string): boolean {
29+
return SELECT_PLACEMENTS_ATTRIBUTE_PERSISTENCE_DENY_SET.has(key.toLowerCase());
30+
}
31+
32+
export function removeSelectPlacementsAttributePersistenceDeniedAttributes(
33+
attributes: Record<string, unknown> | null | undefined,
34+
): Record<string, unknown> {
35+
const filteredAttributes: Record<string, unknown> = {};
36+
const sourceAttributes = attributes || {};
37+
const attributeKeys = Object.keys(sourceAttributes);
38+
39+
for (let i = 0; i < attributeKeys.length; i++) {
40+
const key = attributeKeys[i];
41+
if (!isSelectPlacementsAttributePersistenceDenied(key)) {
42+
filteredAttributes[key] = sourceAttributes[key];
43+
}
44+
}
45+
46+
return filteredAttributes;
47+
}

test/src/tests.spec.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import packageJson from '../../package.json';
22
const packageVersion = packageJson.version;
33
import '../../src/Rokt-Kit';
4+
import {
5+
isSelectPlacementsAttributePersistenceDenied,
6+
removeSelectPlacementsAttributePersistenceDeniedAttributes,
7+
} from '../../src/selectPlacementsAttributePersistence';
48
import { Batch } from '@mparticle/web-sdk/internal';
59

610
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -1098,6 +1102,126 @@ describe('Rokt Forwarder', () => {
10981102
},
10991103
});
11001104
});
1105+
1106+
it('should not send denylisted commerce attributes from the cached user attributes', async () => {
1107+
await (window as any).mParticle.forwarder.init(
1108+
{
1109+
accountId: '123456',
1110+
},
1111+
reportService.cb,
1112+
true,
1113+
null,
1114+
{
1115+
confirmationRef: 'previous-order',
1116+
conversionType: 'purchase',
1117+
PaymentServiceProviderAttribute: 'cached-provider',
1118+
totalPrice: '10.00',
1119+
couponCode: 'SAVE10',
1120+
shippingMethod: 'ground',
1121+
loyaltyTier: 'gold',
1122+
},
1123+
);
1124+
1125+
await (window as any).mParticle.forwarder.selectPlacements({
1126+
identifier: 'test-placement',
1127+
attributes: {
1128+
page: 'checkout',
1129+
},
1130+
});
1131+
1132+
expect((window as any).Rokt.selectPlacementsCalled).toBe(true);
1133+
expect((window as any).Rokt.selectPlacementsOptions).toEqual({
1134+
identifier: 'test-placement',
1135+
attributes: {
1136+
loyaltyTier: 'gold',
1137+
page: 'checkout',
1138+
mpid: '123',
1139+
},
1140+
});
1141+
expect((window as any).mParticle.forwarder.userAttributes).toEqual({
1142+
loyaltyTier: 'gold',
1143+
page: 'checkout',
1144+
});
1145+
});
1146+
1147+
it('should allow explicit commerce attributes for the current call without caching them', async () => {
1148+
await (window as any).mParticle.forwarder.init(
1149+
{
1150+
accountId: '123456',
1151+
},
1152+
reportService.cb,
1153+
true,
1154+
null,
1155+
{
1156+
loyaltyTier: 'gold',
1157+
},
1158+
);
1159+
1160+
await (window as any).mParticle.forwarder.selectPlacements({
1161+
identifier: 'test-placement',
1162+
attributes: {
1163+
confirmationRef: 'current-order',
1164+
conversionType: 'purchase',
1165+
paymentServiceProviderAttribute: 'current-provider',
1166+
totalPrice: '10.00',
1167+
couponCode: 'SAVE10',
1168+
shippingMethod: 'ground',
1169+
page: 'checkout',
1170+
},
1171+
});
1172+
1173+
expect((window as any).Rokt.selectPlacementsCalled).toBe(true);
1174+
expect((window as any).Rokt.selectPlacementsOptions).toEqual({
1175+
identifier: 'test-placement',
1176+
attributes: {
1177+
loyaltyTier: 'gold',
1178+
confirmationRef: 'current-order',
1179+
conversionType: 'purchase',
1180+
paymentServiceProviderAttribute: 'current-provider',
1181+
totalPrice: '10.00',
1182+
couponCode: 'SAVE10',
1183+
shippingMethod: 'ground',
1184+
page: 'checkout',
1185+
mpid: '123',
1186+
},
1187+
});
1188+
expect((window as any).mParticle.forwarder.userAttributes).toEqual({
1189+
loyaltyTier: 'gold',
1190+
page: 'checkout',
1191+
});
1192+
});
1193+
1194+
it('should not cache denylisted commerce attributes set through setUserAttribute', async () => {
1195+
await (window as any).mParticle.forwarder.init(
1196+
{
1197+
accountId: '123456',
1198+
},
1199+
reportService.cb,
1200+
true,
1201+
null,
1202+
{},
1203+
);
1204+
1205+
(window as any).mParticle.forwarder.setUserAttribute('paymentServiceProviderAttribute', 'cached-provider');
1206+
(window as any).mParticle.forwarder.setUserAttribute('favoriteStore', 'test-store');
1207+
1208+
await (window as any).mParticle.forwarder.selectPlacements({
1209+
identifier: 'test-placement',
1210+
attributes: {},
1211+
});
1212+
1213+
expect((window as any).Rokt.selectPlacementsCalled).toBe(true);
1214+
expect((window as any).Rokt.selectPlacementsOptions).toEqual({
1215+
identifier: 'test-placement',
1216+
attributes: {
1217+
favoriteStore: 'test-store',
1218+
mpid: '123',
1219+
},
1220+
});
1221+
expect((window as any).mParticle.forwarder.userAttributes).toEqual({
1222+
favoriteStore: 'test-store',
1223+
});
1224+
});
11011225
});
11021226

11031227
describe('Identity handling', () => {
@@ -2989,6 +3113,30 @@ describe('Rokt Forwarder', () => {
29893113
});
29903114
expect((window as any).mParticle.forwarder.filters.filteredUser.getMPID()).toBe('123');
29913115
});
3116+
3117+
it('should not cache denylisted commerce attributes from the filtered user', () => {
3118+
(window as any).mParticle.forwarder.onUserIdentified({
3119+
getAllUserAttributes: function () {
3120+
return {
3121+
confirmationRef: 'previous-order',
3122+
conversionType: 'purchase',
3123+
currency: 'USD',
3124+
paymentServiceProvider: 'test-provider',
3125+
'test-attribute': 'test-value',
3126+
};
3127+
},
3128+
getMPID: function () {
3129+
return '123';
3130+
},
3131+
getUserIdentities: function () {
3132+
return { userIdentities: {} };
3133+
},
3134+
});
3135+
3136+
expect((window as any).mParticle.forwarder.userAttributes).toEqual({
3137+
'test-attribute': 'test-value',
3138+
});
3139+
});
29923140
});
29933141

29943142
describe('#workspaceIdSync', () => {
@@ -6189,6 +6337,50 @@ describe('Rokt Forwarder', () => {
61896337
});
61906338
});
61916339

6340+
describe('#isSelectPlacementsAttributePersistenceDenied', () => {
6341+
it('should identify denylisted attributes case-insensitively', () => {
6342+
expect(isSelectPlacementsAttributePersistenceDenied('confirmationref')).toBe(true);
6343+
expect(isSelectPlacementsAttributePersistenceDenied('confirmationRef')).toBe(true);
6344+
expect(isSelectPlacementsAttributePersistenceDenied('CONFIRMATIONREF')).toBe(true);
6345+
expect(isSelectPlacementsAttributePersistenceDenied('paymentServiceProvider')).toBe(true);
6346+
expect(isSelectPlacementsAttributePersistenceDenied('cartItems')).toBe(true);
6347+
expect(isSelectPlacementsAttributePersistenceDenied('conversionType')).toBe(true);
6348+
});
6349+
6350+
it('should return false for attributes that are not denylisted', () => {
6351+
expect(isSelectPlacementsAttributePersistenceDenied('loyaltyTier')).toBe(false);
6352+
expect(isSelectPlacementsAttributePersistenceDenied('favoriteStore')).toBe(false);
6353+
});
6354+
});
6355+
6356+
describe('#removeSelectPlacementsAttributePersistenceDeniedAttributes', () => {
6357+
it('should remove denylisted attributes case-insensitively', () => {
6358+
const attributes = {
6359+
confirmationRef: 'previous-order',
6360+
PaymentServiceProvider: 'test-provider',
6361+
cartItems: [{ sku: 'test-sku' }],
6362+
conversionType: 'purchase',
6363+
loyaltyTier: 'gold',
6364+
};
6365+
6366+
expect(removeSelectPlacementsAttributePersistenceDeniedAttributes(attributes)).toEqual({
6367+
loyaltyTier: 'gold',
6368+
});
6369+
expect(attributes).toEqual({
6370+
confirmationRef: 'previous-order',
6371+
PaymentServiceProvider: 'test-provider',
6372+
cartItems: [{ sku: 'test-sku' }],
6373+
conversionType: 'purchase',
6374+
loyaltyTier: 'gold',
6375+
});
6376+
});
6377+
6378+
it('should return an empty object for null or undefined attributes', () => {
6379+
expect(removeSelectPlacementsAttributePersistenceDeniedAttributes(null)).toEqual({});
6380+
expect(removeSelectPlacementsAttributePersistenceDeniedAttributes(undefined)).toEqual({});
6381+
});
6382+
});
6383+
61926384
describe('#hashEventMessage', () => {
61936385
it('should hash event message using generateHash in the proper order', () => {
61946386
const eventName = 'Test Event';

0 commit comments

Comments
 (0)