Skip to content

Commit

Permalink
feat(ga4): added support of filter user traits
Browse files Browse the repository at this point in the history
  • Loading branch information
mihir-4116 committed Oct 3, 2023
1 parent f1b5dd6 commit 142a5d1
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 15 deletions.
6 changes: 3 additions & 3 deletions src/v0/destinations/ga4/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const responseBuilder = (message, { Config }) => {
}
break;
default:
throw ConfigurationError('Invalid type of client');
throw new ConfigurationError('Invalid type of client');

Check warning on line 109 in src/v0/destinations/ga4/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/ga4/transform.js#L109

Added line #L109 was not covered by tests
}

let payload = {};
Expand All @@ -119,7 +119,7 @@ const responseBuilder = (message, { Config }) => {
payload.name = evConfigEvent;
payload.params = constructPayload(message, mappingConfig[name]);

const { items, mapRootLevelPropertiesToGA4ItemsArray } = getItemsArray(message, item, itemList)
const { items, mapRootLevelPropertiesToGA4ItemsArray } = getItemsArray(message, item, itemList);

if (items.length > 0) {
payload.params.items = items;
Expand Down Expand Up @@ -233,7 +233,7 @@ const responseBuilder = (message, { Config }) => {
}

// Prepare GA4 user properties
const userProperties = prepareUserProperties(message);
const userProperties = prepareUserProperties(message, Config.piiPropertiesToIgnore);
if (!isEmptyObject(userProperties)) {
rawPayload.user_properties = userProperties;
}
Expand Down
36 changes: 25 additions & 11 deletions src/v0/destinations/ga4/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,15 @@ const GA4_ITEM_EXCLUSION = [
];

/**
* Remove arrays and objects from transformed payload
* @param {*} params
* @returns
* Remove arrays and objects from transformed payload
* @param {*} params
* @returns
*/
const removeInvalidParams = (params) =>
Object.fromEntries(
Object.entries(params).filter(([key, value]) => key === 'items' || (typeof value !== 'object' && !isEmpty(value))),
Object.entries(params).filter(
([key, value]) => key === 'items' || (typeof value !== 'object' && !isEmpty(value)),
),
);

/**
Expand Down Expand Up @@ -244,10 +246,10 @@ const getItem = (message, isItemsRequired) => {

/**
* Returns items array for ga4 event payload
* @param {*} message
* @param {*} item
* @param {*} itemList
* @returns
* @param {*} message
* @param {*} item
* @param {*} itemList
* @returns
*/
const getItemsArray = (message, item, itemList) => {
let items = [];
Expand All @@ -269,7 +271,7 @@ const getItemsArray = (message, item, itemList) => {
}

return { items, mapRootLevelPropertiesToGA4ItemsArray };
}
};
/**
* get exclusion list for a particular event
* ga4ExclusionList contains the sourceKeys that are already mapped
Expand Down Expand Up @@ -398,13 +400,25 @@ const isValidUserProperty = (key, value) => {
/**
* Function to validate and prepare user_properties
* @param {*} message
* @param {*} piiPropertiesToIgnore
* @returns
*/
const prepareUserProperties = (message) => {
const prepareUserProperties = (message, piiPropertiesToIgnore = []) => {
// Exclude PII user traits
const piiProperties = [];
if (piiPropertiesToIgnore.length > 0) {
piiPropertiesToIgnore.forEach((property) => {
if (typeof property.piiProperty === 'string' && property.piiProperty.trim() !== '') {
piiProperties.push(property.piiProperty.trim());
}
});
}

const userProperties = extractCustomFields(
message,
{},
['properties.user_properties', 'context.traits'],
GA4_RESERVED_USER_PROPERTY_EXCLUSION,
[...GA4_RESERVED_USER_PROPERTY_EXCLUSION, ...piiProperties],
);

const validatedUserProperties = Object.entries(userProperties)
Expand Down
252 changes: 251 additions & 1 deletion src/v0/destinations/ga4/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { validateEventName, prepareUserProperties, removeInvalidParams } = requir

const userPropertyData = [
{
description: "Should validate and prepare user_properties",
description: 'Should validate and prepare user_properties',
input: {
userId: 'user@1',
group_id: 'group@1',
Expand Down Expand Up @@ -163,6 +163,256 @@ describe('Google Analytics 4 utils test', () => {
});

describe('prepareUserProperties function tests', () => {
// Empty message and context returns empty object
it('should return empty object when message and context are empty', () => {
// Arrange
const message = {};
const context = {};

// Act
const result = prepareUserProperties(message, []);

// Assert
expect(result).toEqual({});
});

// Filters out reserved and PII properties
it('should filter out reserved and PII properties', () => {
// Arrange
const message = {
context: {
traits: {
property3: 'value3',
property4: 'value4',
pii_property3: 'pii_value3',
pii_property4: 'pii_value4',
},
},
properties: {
user_properties: {
property1: 'value1',
property2: 'value2',
pii_property1: 'pii_value1',
pii_property2: 'pii_value2',
},
},
};

const piiPropertiesToIgnore = [
{ piiProperty: 'pii_property1' },
{ piiProperty: 'pii_property2' },
{ piiProperty: 'pii_property3' },
{ piiProperty: 'pii_property4' },
];

// Act
const result = prepareUserProperties(message, piiPropertiesToIgnore);

// Assert
expect(result).toEqual({
property1: { value: 'value1' },
property2: { value: 'value2' },
property3: { value: 'value3' },
property4: { value: 'value4' },
});
});

// Validates user properties and returns them in expected format
it('should validate user properties and return them in expected format', () => {
// Arrange
const message = {
context: {
traits: {
valid_property3: 'value3',
_invalid_property3: '12_invalid_value3',
valid_property4: 'value4',
invalid_property4: [],
},
},
properties: {
user_properties: {
valid_property1: 'value1',
'12invalid_property1': 'ga_invalid_value1',
valid_property2: 'value2',
ga_invalid_property2: 'google_invalid_value2',
},
},
};
// Act
const result = prepareUserProperties(message, []);

// Assert
expect(result).toEqual({
valid_property1: { value: 'value1' },
valid_property2: { value: 'value2' },
valid_property3: { value: 'value3' },
valid_property4: { value: 'value4' },
});
});

// Invalid user properties are filtered out

// User properties with invalid value types are filtered out
it('should filter out user properties with invalid value types', () => {
// Arrange
const message = {
context: {
traits: {
valid_property3: 'value3',
invalid_property3: { 456: 'value3' },
valid_property4: 'value4',
invalid_property4: '01234567890123456789012345678901234567890123456789',
},
},
properties: {
user_properties: {
valid_property1: 'value1',
invalid_property1: [123, 456],
valid_property2: 'value2',
},
},
};

// Act
const result = prepareUserProperties(message, []);

// Assert
expect(result).toEqual({
valid_property1: { value: 'value1' },
valid_property2: { value: 'value2' },
valid_property3: { value: 'value3' },
valid_property4: { value: 'value4' },
});
});

// PII properties are filtered out
it('should filter out PII properties from user_properties', () => {
// Arrange
const message = {
properties: {
user_properties: {
property1: 'value1',
property2: 'value2',
pii_property1: 'pii_value1',
pii_property2: 'pii_value2',
},
},
};
const piiPropertiesToIgnore = [
{ piiProperty: 'pii_property1' },
{ piiProperty: 'pii_property2' },
];

// Act
const result = prepareUserProperties(message, piiPropertiesToIgnore);

// Assert
expect(result).toEqual({
property1: { value: 'value1' },
property2: { value: 'value2' },
});
});

// PII properties are undefined
it('should return undefined when user_properties is undefined', () => {
// Arrange
const message = {
properties: {
user_properties: {
property1: 'value1',
property2: 'value2',
pii_property1: 'pii_value1',
pii_property2: 'pii_value2',
},
},
};
const piiPropertiesToIgnore = undefined;

// Act
const result = prepareUserProperties(message, piiPropertiesToIgnore);

// Assert
expect(result).toEqual({
pii_property1: { value: 'pii_value1' },
pii_property2: { value: 'pii_value2' },
property1: { value: 'value1' },
property2: { value: 'value2' },
});
});

// User properties with valid keys and values are returned in expected format
it('should return user properties with valid keys and values in expected format', () => {
// Arrange
const message = {
properties: {
user_properties: {
property1: 'value1',
property2: 'value2',
},
},
};

// Act
const result = prepareUserProperties(message, []);

// Assert
expect(result).toEqual({
property1: { value: 'value1' },
property2: { value: 'value2' },
});
});

// User properties with valid keys but invalid values are filtered out
it('should filter out user properties with invalid values', () => {
// Arrange
const message = {
properties: {
user_properties: {
validKey1: 'validValue1',
validKey2: 'validValue2',
invalidKey1: '',
invalidKey2:
'invalidValueThatIsTooLongInvalidValueThatIsTooLongInvalidValueThatIsTooLongInvalidValueThatIsTooLong',
validKey4: true,
},
},
};
const piiPropertiesToIgnore = [];

// Act
const result = prepareUserProperties(message, piiPropertiesToIgnore);

// Assert
expect(result).toEqual({
validKey1: { value: 'validValue1' },
validKey2: { value: 'validValue2' },
validKey4: { value: true },
});
});
// User properties with keys starting with reserved prefixes are filtered out
it('should filter out user properties with keys starting with reserved prefixes', () => {
// Arrange
const message = {
properties: {
user_properties: {
google_property: 'value1',
ga_property: 'value2',
firebase_property: 'value3',
valid_property: 'value4',
},
},
};
const piiPropertiesToIgnore = [];

// Act
const result = prepareUserProperties(message, piiPropertiesToIgnore);

// Assert
expect(result).toEqual({
valid_property: { value: 'value4' },
});
});

userPropertyData.forEach((dataPoint) => {
it(`${dataPoint.description}`, () => {
try {
Expand Down

0 comments on commit 142a5d1

Please sign in to comment.