feat(googleTag): add dataLayerVariables

This commit is contained in:
Kevin Franklin Kim 2025-02-17 09:40:06 +01:00
parent b677df10e4
commit bcb3f90e8e
No known key found for this signature in database
13 changed files with 263 additions and 166 deletions

View File

@ -108,6 +108,7 @@ linters:
- asciicheck # checks that all code identifiers does not have non-ASCII symbols in the name [fast: true, auto-fix: false]
- bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false]
- bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false]
- canonicalheader # Checks whether net/http.Header uses canonical header [fast: false, auto-fix: false]
- containedctx # containedctx is a linter that detects struct contained context.Context field [fast: false, auto-fix: false]
- contextcheck # check whether the function uses a non-inherited context [fast: false, auto-fix: false]
- copyloopvar # (go >= 1.22) copyloopvar is a linter detects places where loop variables are copied [fast: true, auto-fix: false]
@ -117,6 +118,7 @@ linters:
- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false]
- errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false]
- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
- fatcontext #Detects nested contexts in loops [fast: false, auto-fix: false]
#- forbidigo # Forbids identifiers [fast: false, auto-fix: false]
- forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
- gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid. [fast: true, auto-fix: false]
@ -137,11 +139,12 @@ linters:
- intrange # (go >= 1.22) intrange is a linter to find places where for loops could make use of an integer range. [fast: true, auto-fix: false]
- loggercheck # (logrlint) Checks key value pairs for common logger libraries (kitlog,klog,logr,zap). [fast: false, auto-fix: false]
- makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false]
- misspell # Finds commonly misspelled English words [fast: true, auto-fix: true]
- mirror # reports wrong mirror patterns of bytes/strings usage [fast: false, auto-fix: true]
- misspell # Finds commonly misspelled English words [fast: true, auto-fix: true]
- musttag # enforce field tags in (un)marshaled structs [fast: false, auto-fix: false]
- nakedret # Checks that functions with naked returns are not longer than a maximum size (can be zero). [fast: true, auto-fix: false]
- nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
- nilnesserr # Reports constructs that checks for err != nil, but returns a different nil value error.
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false]
- noctx # Finds sending http request without context.Context [fast: false, auto-fix: false]
- nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: true]
@ -156,7 +159,6 @@ linters:
- spancheck # Checks for mistakes with OpenTelemetry/Census spans. [fast: false, auto-fix: false]
- sqlclosecheck # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed. [fast: false, auto-fix: false]
- stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false]
- tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [fast: false, auto-fix: false]
- testableexamples # linter checks if examples are testable (have an expected output) [fast: true, auto-fix: false]
- testifylint # Checks usage of github.com/stretchr/testify. [fast: false, auto-fix: false]
- testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
@ -166,8 +168,6 @@ linters:
- usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. [fast: true, auto-fix: false]
- wastedassign # Finds wasted assignment statements [fast: false, auto-fix: false]
- whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc. [fast: true, auto-fix: true]
- canonicalheader # Checks whether net/http.Header uses canonical header [fast: false, auto-fix: false]
- fatcontext #Detects nested contexts in loops [fast: false, auto-fix: false]
## Don't enable
#- cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false]
@ -177,6 +177,7 @@ linters:
#- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
#- err113 # Go linter to check the errors handling expressions [fast: false, auto-fix: false]
#- exhaustruct # Checks if all structure fields are initialized [fast: false, auto-fix: false]
#- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions. [auto-fix]
#- gci # Gci controls Go package import order and makes it always deterministic. [fast: true, auto-fix: true]
#- gochecknoglobals # Check that no global variables exist. [fast: false, auto-fix: false]
#- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]

View File

@ -5,6 +5,8 @@ type GoogleTag struct {
TagID string `json:"tagId" yaml:"tagId"`
// Whether a page_view should be sent on initial load
SendPageView bool `json:"sendPageView" yaml:"sendPageView"`
// Data layer variables to be added to the event settings
DataLayerVariables map[string]string `json:"dataLayerVariables" yaml:"dataLayerVariables"`
// TypeScript settings
TypeScript TypeScript `json:"typeScript" yaml:"typeScript"`
}

View File

@ -70,7 +70,7 @@ func Server(l *slog.Logger, tm *tagmanager.TagManager, cfg config.Emarsys) error
return errors.Wrap(err, "failed to upsert event trigger: "+event)
}
if _, err := tm.UpsertTag(servertagx.NewEmarsys(event, merchantID, tagTemplate, eventTrigger)); err != nil {
if _, err := tm.UpsertTag(servertagx.NewEmarsys(event, merchantID, cfg.TestMode, cfg.DebugMode, tagTemplate, eventTrigger)); err != nil {
return err
}
}

View File

@ -1,6 +1,8 @@
package tag
import (
"strconv"
"github.com/foomo/sesamy-cli/pkg/utils"
"google.golang.org/api/tagmanager/v2"
)
@ -9,7 +11,7 @@ func EmarsysName(v string) string {
return "Emarsys - " + v
}
func NewEmarsys(name string, merchantID *tagmanager.Variable, template *tagmanager.CustomTemplate, triggers ...*tagmanager.Trigger) *tagmanager.Tag {
func NewEmarsys(name string, merchantID *tagmanager.Variable, testMode, debugMode bool, template *tagmanager.CustomTemplate, triggers ...*tagmanager.Trigger) *tagmanager.Tag {
return &tagmanager.Tag{
FiringTriggerId: utils.TriggerIDs(triggers),
Name: EmarsysName(name),
@ -28,12 +30,12 @@ func NewEmarsys(name string, merchantID *tagmanager.Variable, template *tagmanag
{
Key: "isTestMode",
Type: "boolean",
Value: "false",
Value: strconv.FormatBool(testMode),
},
{
Key: "isDebugMode",
Type: "boolean",
Value: "false",
Value: strconv.FormatBool(debugMode),
},
},
Type: utils.TemplateType(template),

View File

@ -81,9 +81,10 @@ ___SANDBOXED_JS_FOR_SERVER___
const Math = require('Math');
const JSON = require('JSON');
const setCookie = require('setCookie');
const sendHttpGet = require('sendHttpGet');
const setResponseBody = require('setResponseBody');
const setResponseStatus = require('setResponseStatus');
const getRemoteAddress = require('getRemoteAddress');
const encodeUriComponent = require('encodeUriComponent');
const getAllEventData = require('getAllEventData');
const getRequestHeader = require('getRequestHeader');
@ -99,22 +100,32 @@ const merchantUrl = 'https://recommender.scarabresearch.com/merchants/'+data.mer
// --- Consent ---
if (!isConsentGivenOrNotRequired()) {
return data.gtmOnSuccess();
return data.gtmOnSuccess();
}
// --- Main ---
const mappedData = mapEventData();
const cookieList = ["s", "cdv", "xp", "fc"];
const headerList = ["referer", "user-agent"];
const requestUrl = merchantUrl+'?'+serializeData(mappedData);
const requestOptions = {
headers: generateRequestHeaders(headerList, cookieList),
timeout: 500,
};
return sendHttpGet(merchantUrl+'?'+serializeData(mappedData)).then((result) => {
return sendHttpGet(requestUrl, requestOptions).then((result) => {
if (result.statusCode >= 200 && result.statusCode < 300) {
data.gtmOnSuccess();
if (result.headers['set-cookie']) {
setResponseCookies(result.headers['set-cookie']);
}
data.gtmOnSuccess();
} else {
logToConsole('[FAILURE]', {
request: mappedData,
eventData: eventData,
});
data.gtmOnFailure();
data.gtmOnFailure();
}
});
@ -122,10 +133,11 @@ return sendHttpGet(merchantUrl+'?'+serializeData(mappedData)).then((result) => {
function mapEventData() {
const mappedData = {
email: eventData.emarsys_email || null,
customerId: eventData.user_id || null,
sessionId: getCookieValues('emarsys_s')[0] || eventData.ga_session_id,
pageViewId: eventData.page_view_id || generatePageViewId(),
isNewPageView: !eventData.page_view_id,
pageViewId: eventData.emarsys_page_view_id || generatePageViewId(),
isNewPageView: !eventData.emarsys_page_view_id,
visitorId: getCookieValues('emarsys_cdv')[0] || eventData.client_id,
referrer: eventData.page_referrer || null,
orderId: null,
@ -136,27 +148,12 @@ function mapEventData() {
};
switch (eventData.event_name) {
// custom events
case 'emarsys_cart': {
case 'page_view': {
mappedData.cart = serializeItems(eventData.items || []);
break;
}
case 'emarsys_category': {
mappedData.category = eventData.item_list_id;
break;
}
case 'emarsys_purchase': {
mappedData.orderId = eventData.transaction_id;
mappedData.order = serializeItems(eventData.items || []);
break;
}
case 'emarsys_view': {
mappedData.view = serializeItem(eventData.items[0] || {});
break;
}
// standard ecommerce evens
case 'view_cart': {
mappedData.cart = serializeItems(eventData.items || []);
case 'view_item': {
mappedData.view = serializeItem(eventData.items[0] || {}, false);
break;
}
case 'view_item_list': {
@ -166,10 +163,12 @@ function mapEventData() {
case 'purchase': {
mappedData.orderId = eventData.transaction_id;
mappedData.order = serializeItems(eventData.items || []);
break;
}
case 'view_item': {
mappedData.view = serializeItem(eventData.items[0] || {});
if (eventData.tax) {
mappedData.order[0].price += eventData.tax;
}
if (eventData.shipping) {
mappedData.order[0].price += eventData.shipping;
}
break;
}
}
@ -179,20 +178,20 @@ function mapEventData() {
function serializeItems(items) {
const ret = [];
items.forEach((item) => {
ret.push(serializeItem(item));
ret.push(serializeItem(item, true));
});
return ret.join('|');
}
function serializeItem(item) {
function serializeItem(item, full) {
const ret = [];
if (item.item_id) {
ret.push('i:'+item.item_id);
}
if (item.price) {
if (full && item.price) {
ret.push('p:'+item.price);
}
if (item.quantity) {
if (full && item.quantity) {
ret.push('q:'+item.quantity);
}
return ret.join(',');
@ -207,6 +206,9 @@ function serializeData(mappedData) {
if (mappedData.isNewPageView) {
slist.push("xp=1");
}
if (mappedData.email) {
slist.push("eh=" + encodeUriComponent(mappedData.email));
}
if (mappedData.customerId) {
slist.push("ci=" + encodeUriComponent(mappedData.customerId));
}
@ -250,14 +252,59 @@ function generatePageViewId() {
}
function isConsentGivenOrNotRequired() {
if (data.adStorageConsent !== 'required') {
return true;
if (data.adStorageConsent !== 'required') {
return true;
}
if (eventData.consent_state) {
return !!eventData.consent_state.ad_storage;
}
const xGaGcs = eventData['x-ga-gcs'] || ''; // x-ga-gcs is a string like "G110"
return xGaGcs[2] === '1';
}
function setResponseCookies(cookieList) {
for (let i = 0; i < cookieList.length; i++) {
let cookieArray = cookieList[i].split("; ").map((pair) => pair.split("="));
let cookieJSON = "";
for (let j = 1; j < cookieArray.length; j++) {
if (j === 1) cookieJSON += "{";
if (cookieArray[j].length > 1) cookieJSON += '"' + cookieArray[j][0] + '": "' + cookieArray[j][1] + '"';
else cookieJSON += '"' + cookieArray[j][0] + '": ' + true;
if (j + 1 < cookieArray.length) cookieJSON += ",";
else cookieJSON += "}";
}
if (eventData.consent_state) {
return !!eventData.consent_state.ad_storage;
setCookie(cookieArray[0][0], cookieArray[0][1], JSON.parse(cookieJSON));
}
}
function generateRequestHeaders(headerList, cookieList) {
let headers = {};
let cookies = [];
for (let i = 0; i < headerList.length; i++) {
let headerName = headerList[i];
let headerValue = getRequestHeader(headerName);
if (headerValue) {
headers[headerName] = getRequestHeader(headerName);
}
const xGaGcs = eventData['x-ga-gcs'] || ''; // x-ga-gcs is a string like "G110"
return xGaGcs[2] === '1';
}
headers.cookie = "";
for (let i = 0; i < cookieList.length; i++) {
let cookieName = cookieList[i];
let cookieValue = getCookieValues(cookieName);
if (cookieValue && cookieValue.length) {
cookies.push(cookieName + "=" + cookieValue[0]);
}
}
headers.cookie = cookies.join("; ");
headers["X-Forwarded-For"] = getRemoteAddress();
return headers;
}
@ -392,6 +439,75 @@ ___SERVER_PERMISSIONS___
]
},
"isRequired": true
},
{
"instance": {
"key": {
"publicId": "set_cookies",
"versionId": "1"
},
"param": [
{
"key": "allowedCookies",
"value": {
"type": 2,
"listItem": [
{
"type": 3,
"mapKey": [
{
"type": 1,
"string": "name"
},
{
"type": 1,
"string": "domain"
},
{
"type": 1,
"string": "path"
},
{
"type": 1,
"string": "secure"
},
{
"type": 1,
"string": "session"
}
],
"mapValue": [
{
"type": 1,
"string": "*"
},
{
"type": 1,
"string": "*"
},
{
"type": 1,
"string": "*"
},
{
"type": 1,
"string": "any"
},
{
"type": 1,
"string": "any"
}
]
}
]
}
}
]
},
"clientAnnotations": {
"isEditedByUser": true
},
"isRequired": true
}
]

View File

@ -32,6 +32,12 @@ func Web(tm *tagmanager.TagManager, cfg config.Emarsys) error {
}
}
if _, err := googletag.CreateWebDatalayerVariables(tm, map[string]string{
"emarsys_page_view_id": "emarsys.page_view_id",
}); err != nil {
return err
}
{ // create event tags
tagID, err := tm.LookupVariable(googletag.NameGoogleTagID)
if err != nil {

View File

@ -42,7 +42,6 @@ const logToConsole = require('logToConsole');
const generateRandom = require('generateRandom');
const getCookieValues = require('getCookieValues');
const encodeUriComponent = require('encodeUriComponent');
const createArgumentsQueue = require('createArgumentsQueue');
// --- Config
@ -56,16 +55,14 @@ logToConsole(JSON.stringify({
pageViewId: pageViewId,
}));
const gtag = createArgumentsQueue('gtag', 'dataLayer');
// set page view id
gtag('set', 'emarsys', { page_view_id: pageViewId });
let query = [];
if (sessionId) query.push('s='+encodeUriComponent(sessionId));
if (sessionId) query.push('s='+encodeUriComponent(sessionId));
if (pageViewId) query.push('pv='+encodeUriComponent(pageViewId));
gtagSet({emarsys: {page_view_id: pageViewId}});
// call emarsys client
let query = [];
if (sessionId) query.push('s='+encodeUriComponent(sessionId));
if (visitorId) query.push('vi='+encodeUriComponent(visitorId));
if (pageViewId) query.push('pv='+encodeUriComponent(pageViewId));
sendPixel("/gtag/js/emarsys?"+query.join('&'));
// Call data.gtmOnSuccess when the tag is finished.
@ -151,103 +148,6 @@ ___WEB_PERMISSIONS___
]
},
"isRequired": true
},
{
"instance": {
"key": {
"publicId": "access_globals",
"versionId": "1"
},
"param": [
{
"key": "keys",
"value": {
"type": 2,
"listItem": [
{
"type": 3,
"mapKey": [
{
"type": 1,
"string": "key"
},
{
"type": 1,
"string": "read"
},
{
"type": 1,
"string": "write"
},
{
"type": 1,
"string": "execute"
}
],
"mapValue": [
{
"type": 1,
"string": "gtag"
},
{
"type": 8,
"boolean": true
},
{
"type": 8,
"boolean": true
},
{
"type": 8,
"boolean": false
}
]
},
{
"type": 3,
"mapKey": [
{
"type": 1,
"string": "key"
},
{
"type": 1,
"string": "read"
},
{
"type": 1,
"string": "write"
},
{
"type": 1,
"string": "execute"
}
],
"mapValue": [
{
"type": 1,
"string": "dataLayer"
},
{
"type": 8,
"boolean": true
},
{
"type": 8,
"boolean": true
},
{
"type": 8,
"boolean": false
}
]
}
]
}
}
]
},
"isRequired": true
}
]

View File

@ -23,11 +23,20 @@ func Web(tm *tagmanager.TagManager, cfg config.GoogleTag) error {
}
{ // setup google tag
settings := map[string]string{
configSettings := map[string]string{
"server_container_url": "https://{{Page Hostname}}",
}
if !cfg.SendPageView {
settings["send_page_view"] = "false"
configSettings["send_page_view"] = "false"
}
eventSettings := map[string]*api.Variable{}
for k, v := range cfg.DataLayerVariables {
dlv, err := tm.UpsertVariable(variable.NewDataLayerVariable(v))
if err != nil {
return err
}
eventSettings[k] = dlv
}
tagID, err := tm.UpsertVariable(commonvariable.NewConstant(NameGoogleTagID, cfg.TagID))
@ -35,11 +44,11 @@ func Web(tm *tagmanager.TagManager, cfg config.GoogleTag) error {
return err
}
settingsVariable, err := tm.UpsertVariable(containervariable.NewGoogleTagConfigurationSettings(NameGoogleTagSettings, settings))
settingsVariable, err := tm.UpsertVariable(containervariable.NewGoogleTagConfigurationSettings(NameGoogleTagSettings, configSettings))
if err != nil {
return err
}
if _, err = tm.UpsertTag(webtag.NewGoogleTag(NameGoogleTag, tagID, settingsVariable)); err != nil {
if _, err = tm.UpsertTag(webtag.NewGoogleTag(NameGoogleTag, tagID, settingsVariable, eventSettings)); err != nil {
return err
}
}
@ -62,11 +71,9 @@ func CreateWebEventTriggers(tm *tagmanager.TagManager, cfg contemplate.Config) (
return nil, err
}
variables := make(map[string]*api.Variable, len(parameters))
for parameterName, parameterValue := range parameters {
if variables[parameterName], err = tm.UpsertVariable(variable.NewDataLayerVariable(parameterValue)); err != nil {
return nil, err
}
variables, err := CreateWebDatalayerVariables(tm, parameters)
if err != nil {
return nil, err
}
if _, err := tm.UpsertVariable(containervariable.NewGoogleTagEventSettings(event, variables)); err != nil {
@ -76,3 +83,17 @@ func CreateWebEventTriggers(tm *tagmanager.TagManager, cfg contemplate.Config) (
return eventParameters, nil
}
func CreateWebDatalayerVariables(tm *tagmanager.TagManager, parameters map[string]string) (map[string]*api.Variable, error) {
previousFolderName := tm.FolderName()
tm.SetFolderName("Sesamy - " + Name)
defer tm.SetFolderName(previousFolderName)
var err error
variables := make(map[string]*api.Variable, len(parameters))
for parameterName, parameterValue := range parameters {
if variables[parameterName], err = tm.UpsertVariable(variable.NewDataLayerVariable(parameterValue)); err != nil {
return nil, err
}
}
return variables, nil
}

View File

@ -5,7 +5,26 @@ import (
"google.golang.org/api/tagmanager/v2"
)
func NewGoogleTag(name string, tagID *tagmanager.Variable, settings *tagmanager.Variable) *tagmanager.Tag {
func NewGoogleTag(name string, tagID *tagmanager.Variable, configSettings *tagmanager.Variable, eventSettings map[string]*tagmanager.Variable) *tagmanager.Tag {
var eventSettingsList []*tagmanager.Parameter
for k, v := range eventSettings {
eventSettingsList = append(eventSettingsList, &tagmanager.Parameter{
Type: "map",
Map: []*tagmanager.Parameter{
{
Key: "parameter",
Type: "template",
Value: k,
},
{
Key: "parameterValue",
Type: "template",
Value: "{{" + v.Name + "}}",
},
},
})
}
ret := &tagmanager.Tag{
FiringTriggerId: []string{trigger.IDInitialization},
Name: name,
@ -15,10 +34,15 @@ func NewGoogleTag(name string, tagID *tagmanager.Variable, settings *tagmanager.
Type: "template",
Value: "{{" + tagID.Name + "}}",
},
{
Key: "eventSettingsTable",
Type: "list",
List: eventSettingsList,
},
{
Key: "configSettingsVariable",
Type: "template",
Value: "{{" + settings.Name + "}}",
Value: "{{" + configSettings.Name + "}}",
},
},
Type: "googtag",

View File

@ -188,6 +188,14 @@
"type": "string",
"description": "Emarsys merchant id"
},
"testMode": {
"type": "boolean",
"description": "Enable test mode"
},
"debugMode": {
"type": "boolean",
"description": "Enable debug mode"
},
"googleConsent": {
"$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent",
"description": "Google Consent settings"
@ -413,6 +421,10 @@
"type": "boolean",
"description": "Whether a page_view should be sent on initial load"
},
"dataLayerVariables": {
"$ref": "#/$defs/map[string]string",
"description": "Data layer variables to be added to the event settings"
},
"typeScript": {
"$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.TypeScript",
"description": "TypeScript settings"
@ -611,6 +623,12 @@
"$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.MicrosoftAdsConversionTag"
},
"type": "object"
},
"map[string]string": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
}
}

View File

@ -43,6 +43,9 @@ googleTag:
tagId: G-PZ5ELRCR31
# Whether a page_view should be sent on initial load
sendPageView: true
# Data layer variables to be added to the event settings
dataLayerVariables:
emarsys_page_view_id: emarsys.page_view_id
# TypeScript settings
typeScript:
# Target directory for generate files
@ -255,6 +258,10 @@ emarsys:
enabled: true
# Emarsys merchant id
merchantId: ''
# Enable test mode
testMode: false
# Enable debug mode
debugMode: false
# Google Consent settings
googleConsent:
# Enable consent mode

View File

@ -122,7 +122,7 @@ func TestNewClient_Server(t *testing.T) {
cmd := c.Service().Accounts.Containers.Workspaces.Templates.List(c.WorkspacePath())
if r, err := cmd.Do(); assert.NoError(t, err) {
dump(t, r)
fmt.Println(r.Template[8].TemplateData)
fmt.Println(r.Template[5].TemplateData)
}
})
}

View File

@ -274,8 +274,8 @@ func TestNewClient_Web(t *testing.T) {
t.Run("list templates", func(t *testing.T) {
cmd := c.Service().Accounts.Containers.Workspaces.Templates.List(c.WorkspacePath())
if r, err := cmd.Do(); assert.NoError(t, err) {
// dump(t, r)
fmt.Println(r.Template[0].TemplateData)
dump(t, r)
fmt.Println(r.Template[2].TemplateData)
}
})
}