diff --git a/.golangci.yml b/.golangci.yml index 675197a..4d411c4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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] diff --git a/go.mod b/go.mod index 10bdf59..c9c8240 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ require ( github.com/fatih/structtag v1.2.0 github.com/foomo/go v0.0.3 github.com/foomo/gocontemplate v0.1.4 - github.com/foomo/sesamy-go v0.8.0 + github.com/foomo/sesamy-go v0.8.1 github.com/invopop/jsonschema v0.13.0 github.com/joho/godotenv v1.5.1 github.com/mitchellh/hashstructure/v2 v2.0.2 - github.com/mitchellh/mapstructure v1.5.0 + github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 github.com/pkg/errors v0.9.1 github.com/pterm/pterm v0.12.80 github.com/spf13/cobra v1.9.0 diff --git a/go.sum b/go.sum index 888b8f0..58be20b 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/foomo/gocontemplate v0.1.4 h1:MYSsoltno9pNMU5NwALd7PNSQ1XjN1tpqMGWAhC github.com/foomo/gocontemplate v0.1.4/go.mod h1:BH8eODDwlqWhasSl86avbtMN3Zfgt4pWwr8/PBPM5v0= github.com/foomo/gostandards v0.2.0 h1:Ryd7TI9yV3Xk5B84DcUDB7KcL3LzQ8NS+TVOrFxTYfA= github.com/foomo/gostandards v0.2.0/go.mod h1:XQx7Ur6vyvxaIe2cQvAthuhPYDe+d2soibqVcXDXOh4= -github.com/foomo/sesamy-go v0.8.0 h1:o3zfJ6/FDpBpy/+fGihiQrePLUWwe34zK4iTa5OlHQU= -github.com/foomo/sesamy-go v0.8.0/go.mod h1:P1EKsMhG8kAPmxeGCACorY4lfDzIaAgCWw9FVi6r3+Y= +github.com/foomo/sesamy-go v0.8.1 h1:5m1ySZB5gW9Dymz8MFggU8DXs7F1AAcV6WW2ieX9DPI= +github.com/foomo/sesamy-go v0.8.1/go.mod h1:9TlGLPABYmjt/louonKy4Na4nmx9RHq7N9KyZ6Sy7Xw= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= @@ -102,8 +102,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -243,8 +243,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T google.golang.org/api v0.221.0 h1:qzaJfLhDsbMeFee8zBRdt/Nc+xmOuafD/dbdgGfutOU= google.golang.org/api v0.221.0/go.mod h1:7sOU2+TL4TxUTdbi0gWgAIg7tH5qBXxoyhtL+9x3biQ= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= diff --git a/pkg/config/emarsys.go b/pkg/config/emarsys.go index 209f843..ea6901d 100644 --- a/pkg/config/emarsys.go +++ b/pkg/config/emarsys.go @@ -9,6 +9,10 @@ type Emarsys struct { Enabled bool `json:"enabled" yaml:"enabled"` // Emarsys merchant id MerchantID string `json:"merchantId" yaml:"merchantId"` + // Enable test mode + TestMode bool `json:"testMode" yaml:"testMode"` + // Enable debug mode + DebugMode bool `json:"debugMode" yaml:"debugMode"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` // Google Tag Manager web container settings diff --git a/pkg/config/googletag.go b/pkg/config/googletag.go index 429c3ed..dfecb1b 100644 --- a/pkg/config/googletag.go +++ b/pkg/config/googletag.go @@ -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"` } diff --git a/pkg/provider/emarsys/README.md b/pkg/provider/emarsys/README.md new file mode 100644 index 0000000..ba4c42e --- /dev/null +++ b/pkg/provider/emarsys/README.md @@ -0,0 +1,127 @@ +# Emarsys Web Extend + +The emarsys server side web extend provider makes use of the [Web Extend Command](https://dev.emarsys.com/docs/web-extend-reference/a1a185e5fbb6b-web-extend-command-implementation). + +## Initialization + +Since we need to trigger multiple commands, we need to ensure we're always sending the same `sessionId`, `visitorId` & `pageViewId` for all calls. +The initialization sets the `emarsys.page_view_id` variable into the `dataLayer` to be sent with each following request. + +```mermaid +sequenceDiagram + participant Data Layer + participant Web Container + participant Server Container + participant Emarsys + Web Container->>+Server Container: initialization (/gtag/js/emarsys) + Server Container->>Emarsys: pageViewId (sesssionId, visitorId) + Server Container->>-Web Container: Cookies (emarsys_cdv, emarsys_s) + Web Container->>Data Layer: emarsys.page_view_id +``` + +## Commands + +### Cart + +NOTE: The default `page_view` event does not contain the `items` so we need to enrich them in the `collect` service. + +```mermaid +sequenceDiagram + participant Web Container + participant Collect + participant Server Container + participant Emarsys + + Web Container->>Collect: page_view + Collect->>+Server Container: enrich: items + Server Container->>Emarsys: cart
Cookies (s, cdv, xp, fc) + Server Container->>-Web Container: Cookies (s, cdv, xp, fc) +``` + +Standard implementation + +```javascript +// The usual commands to identify visitors and report cart contents. +ScarabQueue.push(['cart', [ + {item: 'item_1', price: 19.9, quantity: 1}, + {item: 'item_2', price: 29.7, quantity: 3} +]]); +// Firing the ScarabQueue. Should be the last call on the page, called only once. +ScarabQueue.push(['go']); +``` + +### View + +```mermaid +sequenceDiagram + participant Web Container + participant Server Container + participant Emarsys + + Web Container->>+Server Container: view_item + Server Container->>Emarsys: view
Cookies (s, cdv, xp, fc) + Server Container->>-Web Container: Cookies (s, cdv, xp, fc) +``` + +Standard implementation + +```javascript +// Passing on item ID to report product view. Item ID should match the value listed in the Product Catalog +ScarabQueue.push(['view', 'item_3']); +// Firing the ScarabQueue. Should be the last call on the page, called only once. +ScarabQueue.push(['go']); +``` + +## Category + +```mermaid +sequenceDiagram + participant Web Container + participant Server Container + participant Emarsys + + Web Container->>+Server Container: view_item_list + Server Container->>Emarsys: category
Cookies (s, cdv, xp, fc) + Server Container->>-Web Container: Cookies (s, cdv, xp, fc) +``` + +Standard implementation + +```javascript +// Passing on the category path being visited. Must match the 'category' values listed in the Product Catalog +ScarabQueue.push(['category', 'Bikes > Road Bikes']); +// Firing the ScarabQueue. Should be the last call on the page, called only once. +ScarabQueue.push(['go']); +``` + +## Purchase + +```mermaid +sequenceDiagram + participant Web Container + participant Server Container + participant Emarsys + + Web Container->>+Server Container: purchase + Server Container->>Emarsys: category
Cookies (s, cdv, xp, fc) + Server Container->>-Web Container: Cookies (s, cdv, xp, fc) +``` + +Standard implementation + +```javascript +// Passing on order details. The price values passed on here serve as the basis of our revenue and revenue contribution reports. +ScarabQueue.push(['purchase', { + orderId: '231213', + items: [ + {item: 'item_1', price: 19.9, quantity: 1}, + {item: 'item_2', price: 29.7, quantity: 3} + ] +}]); +// Firing the ScarabQueue. Should be the last call on the page, called only once. +ScarabQueue.push(['go']); +``` + +## References + +- [Web Extend Command](https://dev.emarsys.com/docs/web-extend-reference/a1a185e5fbb6b-web-extend-command-implementationhttps://dev.emarsys.com/docs/web-extend-reference/a1a185e5fbb6b-web-extend-command-implementation) diff --git a/pkg/provider/emarsys/server.go b/pkg/provider/emarsys/server.go index 7930b10..9123042 100644 --- a/pkg/provider/emarsys/server.go +++ b/pkg/provider/emarsys/server.go @@ -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 } } diff --git a/pkg/provider/emarsys/server/tag/emarsys.go b/pkg/provider/emarsys/server/tag/emarsys.go index ca9a1c9..77770a8 100644 --- a/pkg/provider/emarsys/server/tag/emarsys.go +++ b/pkg/provider/emarsys/server/tag/emarsys.go @@ -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), diff --git a/pkg/provider/emarsys/server/template/emarsyswebextendtagdata.go b/pkg/provider/emarsys/server/template/emarsyswebextendtagdata.go index 5fe0c29..809d2d9 100644 --- a/pkg/provider/emarsys/server/template/emarsyswebextendtagdata.go +++ b/pkg/provider/emarsys/server/template/emarsyswebextendtagdata.go @@ -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 } ] diff --git a/pkg/provider/emarsys/web.go b/pkg/provider/emarsys/web.go index dca098e..393adcd 100644 --- a/pkg/provider/emarsys/web.go +++ b/pkg/provider/emarsys/web.go @@ -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 { diff --git a/pkg/provider/emarsys/web/template/emarsysinitializationtagdata.go b/pkg/provider/emarsys/web/template/emarsysinitializationtagdata.go index 892b12a..aba0dc6 100644 --- a/pkg/provider/emarsys/web/template/emarsysinitializationtagdata.go +++ b/pkg/provider/emarsys/web/template/emarsysinitializationtagdata.go @@ -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 } ] diff --git a/pkg/provider/googletag/web.go b/pkg/provider/googletag/web.go index fa63864..77a2d92 100644 --- a/pkg/provider/googletag/web.go +++ b/pkg/provider/googletag/web.go @@ -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 +} diff --git a/pkg/provider/googletag/web/tag/googletag.go b/pkg/provider/googletag/web/tag/googletag.go index b67d64c..fe6c64d 100644 --- a/pkg/provider/googletag/web/tag/googletag.go +++ b/pkg/provider/googletag/web/tag/googletag.go @@ -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", diff --git a/sesamy.schema.json b/sesamy.schema.json index d9583bc..63e7b27 100644 --- a/sesamy.schema.json +++ b/sesamy.schema.json @@ -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" } } } \ No newline at end of file diff --git a/sesamy.yaml b/sesamy.yaml index f70c904..0c61977 100644 --- a/sesamy.yaml +++ b/sesamy.yaml @@ -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 diff --git a/test/tagmanager/clientserver_test.go b/test/tagmanager/clientserver_test.go index d37e896..e558451 100644 --- a/test/tagmanager/clientserver_test.go +++ b/test/tagmanager/clientserver_test.go @@ -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) } }) } diff --git a/test/tagmanager/clientweb_test.go b/test/tagmanager/clientweb_test.go index b914997..1fee09e 100644 --- a/test/tagmanager/clientweb_test.go +++ b/test/tagmanager/clientweb_test.go @@ -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) } }) }