Skip to content
Merged
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
17 changes: 12 additions & 5 deletions src/Rokt-Kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import type { IUserIdentities } from '@mparticle/web-sdk';

// BaseEvent not re-exported from @mparticle/web-sdk/internal, so we import directly from @mparticle/event-models.
import { BaseEvent, CommerceEvent } from '@mparticle/event-models';
import {
isSelectPlacementsAttributePersistenceDenied,
removeSelectPlacementsAttributePersistenceDeniedAttributes,
} from './selectPlacementsAttributePersistence';

interface RoktKitSettings {
accountId: string;
Expand Down Expand Up @@ -1091,7 +1095,7 @@ class RoktKit implements KitInterface {
): string {
const kitSettings = settings as unknown as RoktKitSettings;
const accountId = kitSettings.accountId;
this.userAttributes = filteredUserAttributes || {};
this.userAttributes = removeSelectPlacementsAttributePersistenceDeniedAttributes(filteredUserAttributes);
this._onboardingExpProvider = kitSettings.onboardingExpProvider;

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

public setUserAttribute(key: string, value: unknown): string {
this.userAttributes[key] = value;
if (!isSelectPlacementsAttributePersistenceDenied(key)) {
this.userAttributes[key] = value;
}
return 'Successfully set user attribute for forwarder: ' + name;
}

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

private handleIdentityComplete(user: IMParticleUser, eventType: RoktIdentityEventType, callbackName: string): string {
const filteredUser = user as FilteredUser;
this.userAttributes = user.getAllUserAttributes();
this.userAttributes = removeSelectPlacementsAttributePersistenceDeniedAttributes(user.getAllUserAttributes());
Comment thread
rmi22186 marked this conversation as resolved.
this.pendingIdentityEvents.push(this.buildIdentityEvent(eventType, filteredUser));
return 'Successfully called ' + callbackName + ' for forwarder: ' + name;
}
Expand Down Expand Up @@ -1402,7 +1408,8 @@ class RoktKit implements KitInterface {

private _dispatchPlacements(options: Record<string, unknown>): RoktSelection | Promise<RoktSelection> | undefined {
const attributes = ((options && (options.attributes as Record<string, unknown>)) || {}) as Record<string, unknown>;
const placementAttributes: Record<string, unknown> = { ...this.userAttributes, ...attributes };
const cachedUserAttributes = removeSelectPlacementsAttributePersistenceDeniedAttributes(this.userAttributes);
const placementAttributes: Record<string, unknown> = { ...cachedUserAttributes, ...attributes };

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

this.userAttributes = filteredAttributes;
this.userAttributes = removeSelectPlacementsAttributePersistenceDeniedAttributes(filteredAttributes);

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

Expand Down
47 changes: 47 additions & 0 deletions src/selectPlacementsAttributePersistence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const SELECT_PLACEMENTS_ATTRIBUTE_PERSISTENCE_DENY_LIST = [
'billingaddress1',
'billingaddress2',
'billingcity',
'billingstate',
'billingzipcode',
'cartitems',
'ccbin',
'confirmationref',
'conversiontype',
'country',
'couponcode',
'currency',
'language',
'paymentserviceprovider',
'paymentserviceproviderattribute',
'paymenttype',
'shippingaddress1',
'shippingcity',
'shippingcountry',
'shippingmethod',
'shippingstate',
'shippingzipcode',
'totalprice',
];
const SELECT_PLACEMENTS_ATTRIBUTE_PERSISTENCE_DENY_SET = new Set(SELECT_PLACEMENTS_ATTRIBUTE_PERSISTENCE_DENY_LIST);

export function isSelectPlacementsAttributePersistenceDenied(key: string): boolean {
return SELECT_PLACEMENTS_ATTRIBUTE_PERSISTENCE_DENY_SET.has(key.toLowerCase());
}

export function removeSelectPlacementsAttributePersistenceDeniedAttributes(
attributes: Record<string, unknown> | null | undefined,
): Record<string, unknown> {
const filteredAttributes: Record<string, unknown> = {};
const sourceAttributes = attributes || {};
const attributeKeys = Object.keys(sourceAttributes);

for (let i = 0; i < attributeKeys.length; i++) {
const key = attributeKeys[i];
if (!isSelectPlacementsAttributePersistenceDenied(key)) {
filteredAttributes[key] = sourceAttributes[key];
}
}

return filteredAttributes;
}
192 changes: 192 additions & 0 deletions test/src/tests.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import packageJson from '../../package.json';
const packageVersion = packageJson.version;
import '../../src/Rokt-Kit';
import {
isSelectPlacementsAttributePersistenceDenied,
removeSelectPlacementsAttributePersistenceDeniedAttributes,
} from '../../src/selectPlacementsAttributePersistence';
import { Batch } from '@mparticle/web-sdk/internal';

/* eslint-disable @typescript-eslint/no-explicit-any */
Expand Down Expand Up @@ -1098,6 +1102,126 @@ describe('Rokt Forwarder', () => {
},
});
});

it('should not send denylisted commerce attributes from the cached user attributes', async () => {
await (window as any).mParticle.forwarder.init(
{
accountId: '123456',
},
reportService.cb,
true,
null,
{
confirmationRef: 'previous-order',
conversionType: 'purchase',
PaymentServiceProviderAttribute: 'cached-provider',
totalPrice: '10.00',
couponCode: 'SAVE10',
shippingMethod: 'ground',
loyaltyTier: 'gold',
},
);

await (window as any).mParticle.forwarder.selectPlacements({
identifier: 'test-placement',
attributes: {
page: 'checkout',
},
});

expect((window as any).Rokt.selectPlacementsCalled).toBe(true);
expect((window as any).Rokt.selectPlacementsOptions).toEqual({
identifier: 'test-placement',
attributes: {
loyaltyTier: 'gold',
page: 'checkout',
mpid: '123',
},
});
expect((window as any).mParticle.forwarder.userAttributes).toEqual({
loyaltyTier: 'gold',
page: 'checkout',
});
});

it('should allow explicit commerce attributes for the current call without caching them', async () => {
await (window as any).mParticle.forwarder.init(
{
accountId: '123456',
},
reportService.cb,
true,
null,
{
loyaltyTier: 'gold',
},
);

await (window as any).mParticle.forwarder.selectPlacements({
identifier: 'test-placement',
attributes: {
confirmationRef: 'current-order',
conversionType: 'purchase',
paymentServiceProviderAttribute: 'current-provider',
totalPrice: '10.00',
couponCode: 'SAVE10',
shippingMethod: 'ground',
page: 'checkout',
},
});

expect((window as any).Rokt.selectPlacementsCalled).toBe(true);
expect((window as any).Rokt.selectPlacementsOptions).toEqual({
identifier: 'test-placement',
attributes: {
loyaltyTier: 'gold',
confirmationRef: 'current-order',
conversionType: 'purchase',
paymentServiceProviderAttribute: 'current-provider',
totalPrice: '10.00',
couponCode: 'SAVE10',
shippingMethod: 'ground',
page: 'checkout',
mpid: '123',
},
});
expect((window as any).mParticle.forwarder.userAttributes).toEqual({
loyaltyTier: 'gold',
page: 'checkout',
});
});

it('should not cache denylisted commerce attributes set through setUserAttribute', async () => {
await (window as any).mParticle.forwarder.init(
{
accountId: '123456',
},
reportService.cb,
true,
null,
{},
);

(window as any).mParticle.forwarder.setUserAttribute('paymentServiceProviderAttribute', 'cached-provider');
(window as any).mParticle.forwarder.setUserAttribute('favoriteStore', 'test-store');

await (window as any).mParticle.forwarder.selectPlacements({
identifier: 'test-placement',
attributes: {},
});

expect((window as any).Rokt.selectPlacementsCalled).toBe(true);
expect((window as any).Rokt.selectPlacementsOptions).toEqual({
identifier: 'test-placement',
attributes: {
favoriteStore: 'test-store',
mpid: '123',
},
});
expect((window as any).mParticle.forwarder.userAttributes).toEqual({
favoriteStore: 'test-store',
});
});
});

describe('Identity handling', () => {
Expand Down Expand Up @@ -2989,6 +3113,30 @@ describe('Rokt Forwarder', () => {
});
expect((window as any).mParticle.forwarder.filters.filteredUser.getMPID()).toBe('123');
});

it('should not cache denylisted commerce attributes from the filtered user', () => {
(window as any).mParticle.forwarder.onUserIdentified({
getAllUserAttributes: function () {
return {
confirmationRef: 'previous-order',
conversionType: 'purchase',
currency: 'USD',
paymentServiceProvider: 'test-provider',
'test-attribute': 'test-value',
};
},
getMPID: function () {
return '123';
},
getUserIdentities: function () {
return { userIdentities: {} };
},
});

expect((window as any).mParticle.forwarder.userAttributes).toEqual({
'test-attribute': 'test-value',
});
});
});

describe('#workspaceIdSync', () => {
Expand Down Expand Up @@ -6189,6 +6337,50 @@ describe('Rokt Forwarder', () => {
});
});

describe('#isSelectPlacementsAttributePersistenceDenied', () => {
it('should identify denylisted attributes case-insensitively', () => {
expect(isSelectPlacementsAttributePersistenceDenied('confirmationref')).toBe(true);
expect(isSelectPlacementsAttributePersistenceDenied('confirmationRef')).toBe(true);
expect(isSelectPlacementsAttributePersistenceDenied('CONFIRMATIONREF')).toBe(true);
expect(isSelectPlacementsAttributePersistenceDenied('paymentServiceProvider')).toBe(true);
expect(isSelectPlacementsAttributePersistenceDenied('cartItems')).toBe(true);
expect(isSelectPlacementsAttributePersistenceDenied('conversionType')).toBe(true);
});

it('should return false for attributes that are not denylisted', () => {
expect(isSelectPlacementsAttributePersistenceDenied('loyaltyTier')).toBe(false);
expect(isSelectPlacementsAttributePersistenceDenied('favoriteStore')).toBe(false);
});
});

describe('#removeSelectPlacementsAttributePersistenceDeniedAttributes', () => {
it('should remove denylisted attributes case-insensitively', () => {
const attributes = {
confirmationRef: 'previous-order',
PaymentServiceProvider: 'test-provider',
cartItems: [{ sku: 'test-sku' }],
conversionType: 'purchase',
loyaltyTier: 'gold',
};

expect(removeSelectPlacementsAttributePersistenceDeniedAttributes(attributes)).toEqual({
loyaltyTier: 'gold',
});
expect(attributes).toEqual({
confirmationRef: 'previous-order',
PaymentServiceProvider: 'test-provider',
cartItems: [{ sku: 'test-sku' }],
conversionType: 'purchase',
loyaltyTier: 'gold',
});
});

it('should return an empty object for null or undefined attributes', () => {
expect(removeSelectPlacementsAttributePersistenceDeniedAttributes(null)).toEqual({});
expect(removeSelectPlacementsAttributePersistenceDeniedAttributes(undefined)).toEqual({});
});
});

describe('#hashEventMessage', () => {
it('should hash event message using generateHash in the proper order', () => {
const eventName = 'Test Event';
Expand Down
Loading