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

Ga4/prod #173

Merged
merged 21 commits into from
Dec 21, 2020
Merged
Show file tree
Hide file tree
Changes from 4 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
83 changes: 83 additions & 0 deletions integrations/GA4/ECommerceEventConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const eventName = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eventNamesConfigArray

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// Browsing Section
{
src: ["products searched", "product searched"],
dest: "search",
},
{ src: ["product list viewed"], dest: "view_item_list" },
// { src: ["product list filtered"], dest: "" },

// Promotion Section
{ src: ["promotion viewed"], dest: "view_promotion" },
{ src: ["promotion clicked"], dest: "select_promotion" },

// Ordering Section
{
src: ["product clicked", "products clicked"],
dest: "select_item",
hasItem: true,
},
{ src: ["product viewed"], dest: "view_item", hasItem: true },
{ src: ["product added"], dest: "add_to_cart", hasItem: true },
{ src: ["product removed"], dest: "remove_from_cart", hasItem: true },
{ src: ["cart viewed"], dest: "view_cart", hasItem: true },
{ src: ["checkout started"], dest: "begin_checkout" },
{ src: ["checkout step viewed"], dest: "" },
{ src: ["checkout step completed"], dest: "" },
{ src: ["payment info entered"], dest: "add_payment_info" },
{ src: ["order updated"], dest: "" },
{ src: ["order completed"], dest: "purchase" },
// { src: ["order refunded"], dest: "refund" }, GA4 refund is different it supports two refund, partial and full refund
{ src: ["order cancelled"], dest: "" },

// Coupon Section

//----------
// do I need the two below events
// Wishlist Section
// { src: ["product added to wishlist"], dest: "add_to_wishlist" },
//-------

// Sharing Section
// { src: ["product shared", "cart shared"], dest: "share" },
//---------

// Reviewing Section

// --------
];

const eventParameter = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar as above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

{ src: "query", dest: ["search_term"] },
{ src: "list_id", dest: ["item_list_id", "items.item_list_id"] },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of array, use a flag like storeAlsoInItems: true/false
that will make the parsing simpler?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okkk looking into it

{ src: "category", dest: ["item_list_name", "items.item_list_name"] },
{ src: "promotion_id", dest: ["items.promotion_id"] },
{ src: "creative", dest: ["items.creative_slot"] },
{ src: "name", dest: ["items.promotion_name"] },
{ src: "position", dest: ["location_id", "items.location_id"] },
{ src: "price", dest: ["value"] },
{ src: "currency", dest: ["currency"] },
{ src: "coupon", dest: ["coupon"] },
{ src: "payment_method", dest: ["payment_type"] },
{ src: "affiliation", dest: ["affiliation"] },
{ src: "shipping", dest: ["shipping"] },
{ src: "tax", dest: ["tax"] },
{ src: "affiliation", dest: ["affiliation"] },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove duplicate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

{ src: "total", dest: ["value"] },
{ src: "checkout_id", dest: ["transaction_id"] },
];

const itemParameter = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar as above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

{ src: "product_id", dest: "item_id" },
{ src: "order_id", dest: "item_id" },
{ src: "name", dest: "item_name" },
{ src: "coupon", dest: "coupon" },
{ src: "category", dest: "item_category" },
{ src: "brand", dest: "item_brand" },
{ src: "variant", dest: "item_variant" },
{ src: "price", dest: "price" },
{ src: "quantity", dest: "quantity" },
{ src: "position", dest: "index" },
];

export { eventName, eventParameter, itemParameter };
211 changes: 211 additions & 0 deletions integrations/GA4/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/* eslint-disable class-methods-use-this */
import logger from "../../utils/logUtil";
import ScriptLoader from "../ScriptLoader";
import {
eventName,
eventParameter,
itemParameter,
} from "./ECommerceEventConfig";

export default class GA4 {
constructor(config, analytics) {
this.measurementId = config.measurementId;
this.analytics = analytics;
this.sendUserId = config.sendUserId || false;
this.blockPageView = config.blockPageViewEvent || false;
this.name = "GA4";
}

loadScript(measurementId, userId) {
window.dataLayer = window.dataLayer || [];
window.gtag =
window.gtag ||
function gt() {
// eslint-disable-next-line prefer-rest-params
window.dataLayer.push(arguments);
};
window.gtag("js", new Date());

// This condition is not working, even after disabling page view
// page_view is even getting called on page load
if (this.blockPageView) {
window.gtag("config", measurementId, {
user_id: userId,
send_page_view: false,
});
} else {
window.gtag("config", measurementId);
}

ScriptLoader(
"google-analytics 4",
`https://www.googletagmanager.com/gtag/js?id=${measurementId}`
);
}

init() {
// To do :: check how custom dimension and metrics is used
const userId = this.analytics.userId || this.analytics.anonymousId;
this.loadScript(this.measurementId, userId);
}

// When adding events do everything ion lowercase.
// use underscores instead of spaces
// Register your parameters to show them up in UI even user_id

/* utility functions ---Start here --- */
isLoaded() {
return !!(window.gtag && window.gtag.push !== Array.prototype.push);
}

isReady() {
return !!(window.gtag && window.gtag.push !== Array.prototype.push);
}
/* utility functions --- Ends here --- */

isReservedName(eventName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add the helper methods to GA4/utils ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

const reservedName = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reservedEventNames

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"ad_activeview",
"ad_click",
"ad_exposure",
"ad_impression",
"ad_query",
"adunit_exposure",
"app_clear_data",
"app_install",
"app_update",
"app_remove",
"error",
"first_open",
"first_visit",
"in_app_purchase",
"notification_dismiss",
"notification_foreground",
"notification_open",
"notification_receive",
"os_update",
"screen_view",
"session_start",
"user_engagement",
];

return reservedName.includes(eventName);
}

sendGAEvent(event, props) {
window.gtag("event", event, props);
}

getDestinationEvent(event) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this too to utils

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return eventName.find((p) => p.src.includes(event.toLowerCase()));
}

getDestinationEventProperties(props, destParameter) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As method doc like what is the input and output format with an example

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to use a library that does this? can you check once like get-value we use for transformer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to use a library that does this? can you check once like get-value we use for transformer?

There is a library : " loadash " using that we can do, but loading some external library means loading extra js, isn't if we use our own implementation where we can modify code as required. which means we can move above function to global and make more generic. One of the way I can think is going through loadash code and provide same implementation in our utils.
What do you say, should I use external library or implement on our own .....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we won't be loading a js but yes our total JS SDK size will increase. We can leave it
Can you add the examples for the input and output like
/*
props: {key: val, key2:val2}
destParameter: {..}
output: {...}
*/
getDestinationEventProperties(props, destParameter) {...}

const destinationProperties = {};
const item = {};
Object.keys(props).forEach((key) => {
destParameter.forEach((param) => {
if (key === param.src) {
if (Array.isArray(param.dest)) {
param.dest.forEach((d) => {
const result = d.split(".");
// Here we only support mapping single level object mapping.
// To Do Future Scope :: implement using recursion to handle multi level prop mapping
if (result.length > 1) {
const levelOne = result[0];
const levelTwo = result[1];
item[levelTwo] = props[key];
if (!destinationProperties[levelOne]) {
destinationProperties[levelOne] = [];
destinationProperties[levelOne].push(item);
}
} else {
destinationProperties[result] = props[key];
}
});
} else {
destinationProperties[param.dest] = props[key];
}
}
});
});
return destinationProperties;
}

getDestinationItemProperties(products, item) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot to remove from here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now removed

const items = [];
let obj = {};
products.forEach((p) => {
obj = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better name than obj

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

obj -> eventMappingObj

...this.getDestinationEventProperties(p, itemParameter),
...(item && item[0]),
};
items.push(obj);
});
return items;
}

track(rudderElement) {
let { event } = rudderElement.message;
const { properties } = rudderElement.message;
const { products } = properties;
let destinationProperties = {};
if (!event || this.isReservedName(event)) {
throw Error("Cannot call un-named/reserved named track event");
}

const obj = this.getDestinationEvent(event);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better name like destEventName

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (obj) {
if (products && Array.isArray(products)) {
event = obj.dest;
// eslint-disable-next-line no-const-assign
destinationProperties = this.getDestinationEventProperties(
properties,
eventParameter
);
destinationProperties.items = this.getDestinationItemProperties(
products,
destinationProperties.items
);
} else {
event = obj.dest;
if (!obj.hasItem) {
// eslint-disable-next-line no-const-assign
destinationProperties = this.getDestinationEventProperties(
properties,
eventParameter
);
} else {
// create items
destinationProperties.items = this.getDestinationItemProperties([
properties,
]);
}
}
} else {
destinationProperties = properties;
}

this.sendGAEvent(event, destinationProperties);
}

identify(rudderElement) {
if (this.sendUserId && rudderElement.message.userId) {
const userId = this.analytics.userId || this.analytics.anonymousId;
window.gtag("config", this.measurementId, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works even if identify is not the first call?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, user is identified even if no identify call is made. As soon as ga scripts load he gets identified in google analytics.

user_id: userId,
});
}
window.gtag("set", "user_properties", this.analytics.userTraits);
logger.debug("in GoogleAnalyticsManager identify");
}

page(rudderElement) {
window.gtag(
"event",
"page_view",
(rudderElement.message.context && rudderElement.message.context.page) ||
Copy link
Contributor

@sayan-mitra sayan-mitra Dec 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://support.google.com/analytics/answer/9234069
let's try to map some of our params to the params mentioned in this link,
For ex: page_view ==>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also check if GA4 allows sending campaign info present under rudderElement.message.context.campaign

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added function to support GA4 params and added config to decide if need to send extra params

{}
);
}
}
3 changes: 3 additions & 0 deletions integrations/GA4/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import GA4 from "./browser.js";

export default GA4;
4 changes: 4 additions & 0 deletions integrations/client_server_name.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ const clientToServerNames = {
OPTIMIZELY: "Optimizely",
FULLSTORY: "Fullstory",
TVSQUUARED: "TVSquared",
<<<<<<< HEAD
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve conflict

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

MOENGAGE: "MoEngage",
AM: "Amplitude"
=======
GA4: "Google Analytics 4",
>>>>>>> 23bdbcd... GA4 sdk implementation
};

export { clientToServerNames };
8 changes: 8 additions & 0 deletions integrations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ import * as Optimizely from "./Optimizely";
import * as Bugsnag from "./Bugsnag";
import * as Fullstory from "./Fullstory";
import * as TVSquared from "./TVSquared";
<<<<<<< HEAD
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missed this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed it

import * as MoEngage from "./MoEngage";
import * as Amplitude from "./Amplitude";
=======
import * as GA4 from "./GA4";
>>>>>>> 23bdbcd... GA4 sdk implementation

// the key names should match the destination.name value to keep partity everywhere
// (config-plan name, native destination.name , exported integration name(this one below))
Expand All @@ -43,8 +47,12 @@ const integrations = {
BUGSNAG: Bugsnag.default,
FULLSTORY: Fullstory.default,
TVSQUARED: TVSquared.default,
<<<<<<< HEAD
MOENGAGE: MoEngage.default,
AM: Amplitude.default,
=======
GA4: GA4.default,
>>>>>>> 23bdbcd... GA4 sdk implementation
};

export { integrations };
6 changes: 6 additions & 0 deletions integrations/integration_cname.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ const commonNames = {
Fullstory: "FULLSTORY",
BUGSNAG: "BUGSNAG",
TVSQUARED: "TVSQUARED",
<<<<<<< HEAD
MOENGAGE: "MoEngage",
AM: "AM",
AMPLITUDE: "AM",
Amplitude: "AM"
=======
"Google Analytics 4": "GA4",
GoogleAnalytics4: "GA4",
GA4: "GA4",
>>>>>>> 23bdbcd... GA4 sdk implementation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve conflict

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

};

export { commonNames };