diff --git a/cmd/tagmanager.go b/cmd/tagmanager.go index fc90768..f574acd 100644 --- a/cmd/tagmanager.go +++ b/cmd/tagmanager.go @@ -12,6 +12,7 @@ func NewTagmanager(root *cobra.Command) *cobra.Command { Short: "Provision Google Tag Manager containers", } + tagmanager.NewTags(cmd) tagmanager.NewServer(cmd) tagmanager.NewWeb(cmd) root.AddCommand(cmd) diff --git a/cmd/tagmanager/tags.go b/cmd/tagmanager/tags.go new file mode 100644 index 0000000..c18e144 --- /dev/null +++ b/cmd/tagmanager/tags.go @@ -0,0 +1,46 @@ +package tagmanager + +import ( + conversionlinkerprovider "github.com/foomo/sesamy-cli/pkg/provider/conversionlinker" + criteoprovider "github.com/foomo/sesamy-cli/pkg/provider/criteo" + emarsysprovider "github.com/foomo/sesamy-cli/pkg/provider/emarsys" + facebookprovider "github.com/foomo/sesamy-cli/pkg/provider/facebook" + googleadsprovider "github.com/foomo/sesamy-cli/pkg/provider/googleads" + googleanalyticsprovider "github.com/foomo/sesamy-cli/pkg/provider/googleanalytics" + googletagmanagerprovider "github.com/foomo/sesamy-cli/pkg/provider/googletagmanager" + microsoftadsprovider "github.com/foomo/sesamy-cli/pkg/provider/microsoftads" + tracifyprovider "github.com/foomo/sesamy-cli/pkg/provider/tracify" + umamiprovider "github.com/foomo/sesamy-cli/pkg/provider/umami" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +// NewTags represents the tags command +func NewTags(root *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "tags", + Short: "Print out all available tags", + RunE: func(cmd *cobra.Command, args []string) error { + // Define the data for the first table + data := pterm.TableData{ + {"Name", "Tag"}, + {conversionlinkerprovider.Name, conversionlinkerprovider.Tag}, + {criteoprovider.Name, criteoprovider.Tag}, + {emarsysprovider.Name, emarsysprovider.Tag}, + {facebookprovider.Name, facebookprovider.Tag}, + {googleadsprovider.Name, googleadsprovider.Tag}, + {googleanalyticsprovider.Name, googleanalyticsprovider.Tag}, + {googletagmanagerprovider.Name, googletagmanagerprovider.Tag}, + {microsoftadsprovider.Name, microsoftadsprovider.Tag}, + {tracifyprovider.Name, tracifyprovider.Tag}, + {umamiprovider.Name, umamiprovider.Tag}, + } + + return pterm.DefaultTable.WithHasHeader().WithData(data).Render() + }, + } + + root.AddCommand(cmd) + + return cmd +} diff --git a/pkg/config/facebook.go b/pkg/config/facebook.go index 20a06c7..50ec43b 100644 --- a/pkg/config/facebook.go +++ b/pkg/config/facebook.go @@ -4,14 +4,27 @@ import ( "github.com/foomo/gocontemplate/pkg/contemplate" ) -type Facebook struct { - // Enable provider - Enabled bool `json:"enabled" yaml:"enabled"` - PixelID string `json:"pixelId" yaml:"pixelId"` - APIAccessToken string `json:"apiAccessToken" yaml:"apiAccessToken"` - TestEventToken string `json:"testEventToken" yaml:"testEventToken"` - // Google Consent settings - GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` - // Google Tag Manager server container settings - ServerContainer contemplate.Config `json:"serverContainer" yaml:"serverContainer"` +type ( + Facebook struct { + // Enable provider + Enabled bool `json:"enabled" yaml:"enabled"` + PixelID string `json:"pixelId" yaml:"pixelId"` + APIAccessToken string `json:"apiAccessToken" yaml:"apiAccessToken"` + TestEventToken string `json:"testEventToken" yaml:"testEventToken"` + // Google Consent settings + GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` + // Google Tag Manager server container settings + ServerContainer FacebookServerContainer `json:"serverContainer" yaml:"serverContainer"` + } + FacebookServerContainer struct { + contemplate.Config `json:",inline" yaml:",squash"` + Settings map[string]FacebookConversionAPITag `json:"settings" yaml:"settings"` + } +) + +func (s *FacebookServerContainer) Setting(eventName string) FacebookConversionAPITag { + if value, ok := s.Settings[eventName]; ok { + return value + } + return FacebookConversionAPITag{} } diff --git a/pkg/config/facebookconversionapitag.go b/pkg/config/facebookconversionapitag.go new file mode 100644 index 0000000..f84d1ec --- /dev/null +++ b/pkg/config/facebookconversionapitag.go @@ -0,0 +1,8 @@ +package config + +type FacebookConversionAPITag struct { + // Extend Meta Pixel cookies (fbp/fbc) + ExtendCookies bool `json:"extendCookies" yaml:"extendCookies"` + // Enable Use of HTTP Only Secure Cookie (gtmeec) to Enhance Event Data + EnableEventEnhancement bool `json:"enableEventEnhancement" yaml:"enableEventEnhancement"` +} diff --git a/pkg/config/googleadsconversion.go b/pkg/config/googleadsconversion.go index c6ddda5..5c93a3c 100644 --- a/pkg/config/googleadsconversion.go +++ b/pkg/config/googleadsconversion.go @@ -4,23 +4,20 @@ import ( "github.com/foomo/gocontemplate/pkg/contemplate" ) -type GoogleAdsConversion struct { - // Enable provider - Enabled bool `json:"enabled" yaml:"enabled"` - // Google Tag Manager server container settings - ServerContainer ServerContainer `json:"serverContainer" yaml:"serverContainer"` -} +type ( + GoogleAdsConversion struct { + // Enable provider + Enabled bool `json:"enabled" yaml:"enabled"` + // Google Tag Manager server container settings + ServerContainer GoogleAdsConversionServerContainer `json:"serverContainer" yaml:"serverContainer"` + } + GoogleAdsConversionServerContainer struct { + contemplate.Config `json:",inline" yaml:",squash"` + Settings map[string]GoogleAdsConversionTracking `json:"settings" yaml:"settings"` + } +) -type ServerContainer struct { - contemplate.Config `json:",inline" yaml:",squash"` - Settings map[string]GoogleAdsConversionTracking `json:"settings" yaml:"settings"` -} - -type GoogleAdsConversionTracking struct { - Label string `json:"label" yaml:"label"` -} - -func (s *ServerContainer) Setting(eventName string) GoogleAdsConversionTracking { +func (s *GoogleAdsConversionServerContainer) Setting(eventName string) GoogleAdsConversionTracking { if value, ok := s.Settings[eventName]; ok { return value } diff --git a/pkg/config/googleadsconversiontracking.go b/pkg/config/googleadsconversiontracking.go new file mode 100644 index 0000000..2c1227a --- /dev/null +++ b/pkg/config/googleadsconversiontracking.go @@ -0,0 +1,5 @@ +package config + +type GoogleAdsConversionTracking struct { + Label string `json:"label" yaml:"label"` +} diff --git a/pkg/config/tracify.go b/pkg/config/tracify.go index 0901f72..42e948f 100644 --- a/pkg/config/tracify.go +++ b/pkg/config/tracify.go @@ -11,6 +11,8 @@ type Tracify struct { Token string `json:"token" yaml:"token"` // Tracify customer site id CustomerSiteID string `json:"customerSiteId" yaml:"customerSiteId"` + // Enable stating mode + StagingModeEnabled bool `json:"stagingModeEnabled" yaml:"stagingModeEnabled"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` // Google Tag Manager server container settings diff --git a/pkg/provider/facebook/server.go b/pkg/provider/facebook/server.go index fb8faca..1526129 100644 --- a/pkg/provider/facebook/server.go +++ b/pkg/provider/facebook/server.go @@ -47,7 +47,7 @@ func Server(l *slog.Logger, tm *tagmanager.TagManager, cfg config.Facebook) erro } { // create tags - eventParameters, err := utils.LoadEventParams(cfg.ServerContainer) + eventParameters, err := utils.LoadEventParams(cfg.ServerContainer.Config) if err != nil { return err } @@ -70,7 +70,7 @@ func Server(l *slog.Logger, tm *tagmanager.TagManager, cfg config.Facebook) erro return errors.Wrap(err, "failed to upsert event trigger: "+event) } - if _, err := tm.UpsertTag(servertagx.NewConversionsAPITag(event, pixelID, apiAccessToken, testEventToken, template, eventTrigger)); err != nil { + if _, err := tm.UpsertTag(servertagx.NewConversionsAPITag(event, pixelID, apiAccessToken, testEventToken, cfg.ServerContainer.Setting(event), template, eventTrigger)); err != nil { return err } } diff --git a/pkg/provider/facebook/server/tag/conversionsapitag.go b/pkg/provider/facebook/server/tag/conversionsapitag.go index c4307c0..52caa7d 100644 --- a/pkg/provider/facebook/server/tag/conversionsapitag.go +++ b/pkg/provider/facebook/server/tag/conversionsapitag.go @@ -1,6 +1,9 @@ package tag import ( + "strconv" + + "github.com/foomo/sesamy-cli/pkg/config" "github.com/foomo/sesamy-cli/pkg/utils" "google.golang.org/api/tagmanager/v2" ) @@ -9,7 +12,7 @@ func ConversionsAPITagName(v string) string { return "FB Conversion - " + v } -func NewConversionsAPITag(name string, pixelID, apiAccessToken, testEventCode *tagmanager.Variable, template *tagmanager.CustomTemplate, triggers ...*tagmanager.Trigger) *tagmanager.Tag { +func NewConversionsAPITag(name string, pixelID, apiAccessToken, testEventCode *tagmanager.Variable, settings config.FacebookConversionAPITag, template *tagmanager.CustomTemplate, triggers ...*tagmanager.Trigger) *tagmanager.Tag { return &tagmanager.Tag{ FiringTriggerId: utils.TriggerIDs(triggers), Name: ConversionsAPITagName(name), @@ -33,12 +36,12 @@ func NewConversionsAPITag(name string, pixelID, apiAccessToken, testEventCode *t { Key: "enableEventEnhancement", Type: "boolean", - Value: "false", + Value: strconv.FormatBool(settings.EnableEventEnhancement), }, { Key: "extendCookies", Type: "boolean", - Value: "false", + Value: strconv.FormatBool(settings.ExtendCookies), }, { Key: "actionSource", diff --git a/pkg/provider/googleanalytics/constants.go b/pkg/provider/googleanalytics/constants.go index 8807690..270803e 100644 --- a/pkg/provider/googleanalytics/constants.go +++ b/pkg/provider/googleanalytics/constants.go @@ -7,6 +7,8 @@ const ( NameGoogleAnalyticsGA4ClientTrigger = "Google Analytics GA4 Client" NameGoogleGTagClientTemplate = "Google gtag.js" NameGoogleGTagClient = "Google gtag.js" + NameMPv2UserDataTransformation = "MPv2 User Data" + NameJSONRequestValueTemplate = "JSON Request Value" NameMeasurementProtocolGA4Client = "Measurement Protocol GA4" NameMeasurementProtocolGA4ClientTrigger = "Measurement Protocol GA4 Client" ) diff --git a/pkg/provider/googleanalytics/server.go b/pkg/provider/googleanalytics/server.go index 8eb7660..ca15448 100644 --- a/pkg/provider/googleanalytics/server.go +++ b/pkg/provider/googleanalytics/server.go @@ -2,15 +2,18 @@ package googleanalytics import ( "github.com/foomo/sesamy-cli/pkg/config" - client2 "github.com/foomo/sesamy-cli/pkg/provider/googleanalytics/server/client" + googleanalyticsclient "github.com/foomo/sesamy-cli/pkg/provider/googleanalytics/server/client" containertag "github.com/foomo/sesamy-cli/pkg/provider/googleanalytics/server/tag" - template2 "github.com/foomo/sesamy-cli/pkg/provider/googleanalytics/server/template" + googleanalyticstemplate "github.com/foomo/sesamy-cli/pkg/provider/googleanalytics/server/template" "github.com/foomo/sesamy-cli/pkg/provider/googleanalytics/server/trigger" "github.com/foomo/sesamy-cli/pkg/provider/googleconsent" googleconsentvariable "github.com/foomo/sesamy-cli/pkg/provider/googleconsent/server/variable" "github.com/foomo/sesamy-cli/pkg/tagmanager" serverclient "github.com/foomo/sesamy-cli/pkg/tagmanager/server/client" + servertemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/server/template" + servertransformation "github.com/foomo/sesamy-cli/pkg/tagmanager/server/transformation" servertrigger "github.com/foomo/sesamy-cli/pkg/tagmanager/server/trigger" + servervariable "github.com/foomo/sesamy-cli/pkg/tagmanager/server/variable" "github.com/foomo/sesamy-cli/pkg/utils" "github.com/pkg/errors" ) @@ -43,15 +46,30 @@ func Server(tm *tagmanager.TagManager, cfg config.GoogleAnalytics, redactVisitor if _, err = tm.UpsertTrigger(servertrigger.NewClient(NameMeasurementProtocolGA4ClientTrigger, client)); err != nil { return err } - } - if cfg.GoogleGTag.Enabled { - template, err := tm.UpsertCustomTemplate(template2.NewGoogleGTagClient(NameGoogleGTagClientTemplate)) + userDataTemplate, err := tm.UpsertCustomTemplate(servertemplate.NewJSONRequestValue(NameJSONRequestValueTemplate)) if err != nil { return err } - _, err = tm.UpsertClient(client2.NewGoogleGTag(NameGoogleGTagClient, cfg.GoogleGTag, template)) + userDataVariable, err := tm.UpsertVariable(servervariable.NewMPv2Data("user_data", userDataTemplate)) + if err != nil { + return err + } + + _, err = tm.UpsertTransformation(servertransformation.NewMPv2UserData(NameMPv2UserDataTransformation, userDataVariable, client)) + if err != nil { + return err + } + } + + if cfg.GoogleGTag.Enabled { + template, err := tm.UpsertCustomTemplate(googleanalyticstemplate.NewGoogleGTagClient(NameGoogleGTagClientTemplate)) + if err != nil { + return err + } + + _, err = tm.UpsertClient(googleanalyticsclient.NewGoogleGTag(NameGoogleGTagClient, cfg.GoogleGTag, template)) if err != nil { return err } diff --git a/pkg/provider/googleconsent/server/template/googleconsentmodecheck.go b/pkg/provider/googleconsent/server/template/googleconsentmodecheck.go index f28fcd8..6300e7e 100644 --- a/pkg/provider/googleconsent/server/template/googleconsentmodecheck.go +++ b/pkg/provider/googleconsent/server/template/googleconsentmodecheck.go @@ -10,13 +10,5 @@ func NewGoogleConsentModeCheck(name string) *tagmanager.CustomTemplate { return &tagmanager.CustomTemplate{ Name: name, TemplateData: fmt.Sprintf(GoogleConsentModeCheckData, name), - // oogleapi: Error 400: galleryReference: This field is invalid (or unsupported). - // GalleryReference: &tagmanager.GalleryReference{ - // Host: "github.com", - // Owner: "analytics-engineers", - // Repository: "gtm-server-variable-google-consent-mode-check", - // Signature: "8905ba41f72b510484a3ff9dc27dabaf09c029eb1228e2d1435b5cc2e837cc8d", - // Version: "a31230ca43cdadea1b97ef7dcf76b8e9f8c04725", - // }, } } diff --git a/pkg/provider/tracify/server.go b/pkg/provider/tracify/server.go index 0c3a2b4..cd42ef5 100644 --- a/pkg/provider/tracify/server.go +++ b/pkg/provider/tracify/server.go @@ -64,7 +64,7 @@ func Server(l *slog.Logger, tm *tagmanager.TagManager, cfg config.Tracify) error return errors.Wrap(err, "failed to upsert event trigger: "+event) } - if _, err := tm.UpsertTag(servertagx.NewTracify(event, token, customerSiteID, tagTemplate, eventTrigger)); err != nil { + if _, err := tm.UpsertTag(servertagx.NewTracify(event, token, customerSiteID, tagTemplate, cfg, eventTrigger)); err != nil { return err } } diff --git a/pkg/provider/tracify/server/tag/tracify.go b/pkg/provider/tracify/server/tag/tracify.go index 4849090..541e509 100644 --- a/pkg/provider/tracify/server/tag/tracify.go +++ b/pkg/provider/tracify/server/tag/tracify.go @@ -1,6 +1,9 @@ package tag import ( + "strconv" + + "github.com/foomo/sesamy-cli/pkg/config" "github.com/foomo/sesamy-cli/pkg/utils" "google.golang.org/api/tagmanager/v2" ) @@ -9,7 +12,7 @@ func TracifyName(v string) string { return "Tracify - " + v } -func NewTracify(name string, token, customerSiteID *tagmanager.Variable, template *tagmanager.CustomTemplate, triggers ...*tagmanager.Trigger) *tagmanager.Tag { +func NewTracify(name string, token, customerSiteID *tagmanager.Variable, template *tagmanager.CustomTemplate, cfg config.Tracify, triggers ...*tagmanager.Trigger) *tagmanager.Tag { return &tagmanager.Tag{ FiringTriggerId: utils.TriggerIDs(triggers), Name: TracifyName(name), @@ -28,7 +31,7 @@ func NewTracify(name string, token, customerSiteID *tagmanager.Variable, templat { Key: "isStagingMode", Type: "boolean", - Value: "false", + Value: strconv.FormatBool(cfg.StagingModeEnabled), }, { Key: "token", diff --git a/pkg/provider/tracify/server/template/tracifytagdata.go b/pkg/provider/tracify/server/template/tracifytagdata.go index 26a0a45..f262dbe 100644 --- a/pkg/provider/tracify/server/template/tracifytagdata.go +++ b/pkg/provider/tracify/server/template/tracifytagdata.go @@ -7,7 +7,6 @@ const TracifyTagData = `___INFO___ "id": "cvt_temp_public_id", "version": 1, "securityGroups": [], - "displayName": "Manual Tracify", "displayName": "%s", "brand": { "id": "brand_dummy", @@ -115,6 +114,7 @@ const endpoint = hostname + '/api/v1/events'; const remoteAddress = eventData.ip_override || getRemoteAddress(); const sessionId = eventData.ga_session_id || eventData.session_id; const userAgent = eventData.user_agent || getRequestHeader('User-Agent'); +const pageReferer = eventData.page_referrer; const pageLocation = eventData.page_location || getRequestHeader('Referer'); const timestamp = (eventData['x-sst-system_properties'] || {}).request_start_time_ms || (eventData.timestamp_micros / 1000) || getTimestampMillis(); @@ -149,10 +149,12 @@ return sendHttpRequest(endpoint, options, JSON.stringify(body)).then((result) => // --- Utils --- function mapEventData() { + const url = parseUrl(pageLocation); + // https://tracify.dev/events const event = { // The full URL (domain + path) for which the request is served. - url: pageLocation || null, + url: pageLocation, // The identifier used to indicate on what website the events are occurring. // This id is provided by your account representative. customer_site_id: data.customerSiteId, @@ -163,17 +165,24 @@ function mapEventData() { // The type of event type: '', // Data properties specific to the event type sent - data: {}, + data: { + origin: url.hostname, + }, }; + // Referrer + if (pageReferer) { + event.data.Referer = pageReferer; + } + // Anonymized email address (optional, if available) if (eventData.user_id) { event.identity_data[anonymize(eventData.user_id)] = 1; } // Anonymized session id - if (remoteAddress && userAgent) { + if (remoteAddress && userAgent) { event.identity_data[anonymize(remoteAddress+'|'+userAgent)] = 2; - } + } // Anonymized IP address if (remoteAddress) { event.identity_data[anonymize(remoteAddress)] = 3; @@ -183,46 +192,30 @@ function mapEventData() { event.identity_data[anonymize(userAgent)] = 4; } - const url = parseUrl(pageLocation); switch (eventData.event_name) { case 'tracify_page_view': { event.type = 'pageview'; - event.data = { - origin: url.hostname, - }; event.data = mapUrlParams(url.searchParams, event.data); break; } case 'tracify_product_view': { event.type = 'productview'; - event.data = { - origin: url.hostname, - }; event.data = mapUrlParams(url.searchParams, event.data); break; } // standard evens case 'page_view': { event.type = 'pageview'; - event.data = { - origin: url.hostname, - }; event.data = mapUrlParams(url.searchParams, event.data); break; } case 'view_item': { event.type = 'productview'; - event.data = { - origin: url.hostname, - }; event.data = mapUrlParams(url.searchParams, event.data); break; } case 'add_to_cart': { event.type = 'addtocart'; - event.data = { - origin: url.hostname, - }; event.data = mapItems(eventData.items || [], event.data); break; } @@ -241,7 +234,6 @@ function mapEventData() { case 'conversion': { event.type = 'conversion'; event.data = { - origin: url.hostname, conversion_id: makeString(eventData.transaction_id), currency: eventData.currency, cc: eventData.currency, // required due to API bug @@ -255,14 +247,14 @@ function mapEventData() { } function isConsentGivenOrNotRequired() { - if (data.analyticsStorageConsent !== 'required') { - return true; - } - if (eventData.consent_state) { - return !!eventData.consent_state.analytics_storage; - } - const xGaGcs = eventData['x-ga-gcs'] || ''; // x-ga-gcs is a string like "G101" - return xGaGcs[3] === '1'; + if (data.analyticsStorageConsent !== 'required') { + return true; + } + if (eventData.consent_state) { + return !!eventData.consent_state.analytics_storage; + } + const xGaGcs = eventData['x-ga-gcs'] || ''; // x-ga-gcs is a string like "G101" + return xGaGcs[3] === '1'; } function mapItems(items, val) { diff --git a/pkg/tagmanager/server/template/jsonrequestvalue.go b/pkg/tagmanager/server/template/jsonrequestvalue.go new file mode 100644 index 0000000..0df42e7 --- /dev/null +++ b/pkg/tagmanager/server/template/jsonrequestvalue.go @@ -0,0 +1,14 @@ +package template + +import ( + "fmt" + + "google.golang.org/api/tagmanager/v2" +) + +func NewJSONRequestValue(name string) *tagmanager.CustomTemplate { + return &tagmanager.CustomTemplate{ + Name: name, + TemplateData: fmt.Sprintf(JSONRequestValueData, name), + } +} diff --git a/pkg/tagmanager/server/template/jsonrequestvaluedata.go b/pkg/tagmanager/server/template/jsonrequestvaluedata.go new file mode 100644 index 0000000..b55ceba --- /dev/null +++ b/pkg/tagmanager/server/template/jsonrequestvaluedata.go @@ -0,0 +1,126 @@ +package template + +const JSONRequestValueData = `___INFO___ + +{ + "type": "MACRO", + "id": "cvt_temp_public_id", + "version": 1, + "securityGroups": [], + "displayName": "%s", + "brand": { + "id": "brand_dummy", + "displayName": "sesamy", + "thumbnail": "data:image/jpeg;base64,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qEqaLUwCNxCKZYOe6dFBjPqDoI4rERBIeBK+DcwoAKKtaCJQ+CDXNNpzqBHmCcxwpoi4JJrgyjGuyUw6hlELaeQAclgnHgpfqegeEYRvPCCZjzpIAMAApp7QIXKDg64ETjEUywYBJYAumw8qf4osOWJtRl7cG9iVlHm/IhbCBrmm1OVtwJeMRXJRLxhNIBbVpW3vsRC55EpIAAwDDWJW3uAEmAJNAms8qaYiOYa3K29p8ErIiKQyBXJtclbewbjEVyUUZ0EAmcAoBr8rb1Mx3SRQfE/EwEB9BlbfkgwSaBRY/cs0eR9ElbU5iWRDFn+JoAwvoyOUwaKCYioAIf8ATf8A/9oADAMAAAERAhEAABDzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzglbrzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzilRuyvzzzzzzzzzzzzzzzzzzzzzzzzzzzzywTezzzyvzzzzzzzzzzzzzzzzzzzzzzzzzzywcRbzzzzyvzzzzzzzzzzzzzzzzzzzzzzzzzRDVzzzzzzzyvzzzzzzzzzzzzzzzzzzzzzzgl913zzzzzzzU8fzzzzzzzzzzzzzzzzzzzziUdyzzzzzzzzr1wzzzzzzzzzzzzzzzzzzzywP27zzzzzzzzGO0zzzzzzzzzzzzzzzzzzzgQzZ/zzzzzzznNqwzzzzzzzzzzzzzzzzzzzzh033zzzzzz3PHozzzzzzg4vzzzzzzzzzzzzzyy/zzzzzzzG3wxzzzzzg0zIPzzzzzzzzzzzzzzz/wA88845TNc888884JwSCCD888888888888888/889hL7K08888ymiACCCCD888888888888888/+sqE8ss4GuAHhACCCCCCD888888888888884jG088888utswCCCCCCCCCI888888888888888sY+z+8888+9CCCCCCELLuM88888888888888888sdFiU8888CCCCEJqMc888888888888888888888sc2P188CCFPlcc88888888888888888888888888Mqw4GItO8888882888888888888888888888888sdsM88888oirj88888888888888888888888888888880BqCAq888888888888888888888888888880XSgAAAq888888888888888888888888884rNjACAAAAq888888888888888888888888ompLAAAAAAAEC88888888888888888888882QxiiCAAAAAD+Tc88888888888888888888sJEAAAAAAACbExM8888888888888888888884VAAAAAAAIFU/sc8888888888888888888888oVAAAACABIMM8888888888888888888888888oVAACXbks8888888888888888888888888888oVANEtM8888888888888888888888888888884QePc888888888888888888888888888888888cs8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888//EACMRAQACAgEEAwEBAQAAAAAAAAEAESExQBAgMFBBUXFwYYH/2gAIAQIRAT8Q/hWOW2VHoYC1mKzY9V0dNXPOtVMALiOfZq5oluo7gRO10rs1cveMzG4kRXl6ALRbMSZZjjBLdyu9XG4ZlwBo4xtqYIXGsvANzWMf7MhmwBRxhbdT4T/2OW/ACuJm8CZurZRxtkzMXgR21fgZoJlsIVhx8RtMeYIqtvgdo3PncNo8nT+eDVMTN5QWh4AYSATfgafztBdTIVRM25YAYPAJh9oAdd+Bp/OodJlmOibhagSzvEwkK7d+Bp/IpREcxjQ7EWIpGteo2BPBvwKafiqXfdcGooKAeFBFbfBQ7huY35L7tHfcSRbxUHc1mO79Hcginl/pIiOemjqgj9Iq87exTMINxBH6Rb9Eg+FBufCRLZ9CMI6i301c8SK9urmIIBqL3tXKST4SKd+DMFcgBEdRz4AXUR3B4y6gkV4RM+yAa5D8JIA3Ar0IrBPQgLqI7gnoQTPsgDXoUFQDcAP4N//EACMRAQACAgEDBQEBAAAAAAAAAAEAESExQCAwQRBQUWGhcIH/2gAIAQERAT8Q/hSCKy/YgIj6aOfZBNRT0aOagnhIq9WjlpIviLfY1ckSKeyJg+YAcbEEiPZGwDcMcZQnwxTvsowJ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" + }, + "description": "Returns an JSON request value for the given key.", + "containerContexts": [ + "SERVER" + ] +} + + +___TEMPLATE_PARAMETERS___ + +[ + { + "type": "TEXT", + "name": "key", + "displayName": "JSON key", + "simpleValueType": true, + "valueValidators": [ + { + "type": "NON_EMPTY" + } + ] + } +] + + +___SANDBOXED_JS_FOR_SERVER___ + +// Enter your template code here. +const JSON = require('JSON'); +const logToConsole = require('logToConsole'); +const getRequestBody = require('getRequestBody'); + +const requestBody = getRequestBody(); +const payload = JSON.parse(requestBody); +const value = payload[data.key] || null; + +logToConsole({ + key: data.key, + value: value, +}); + +return value; + + +___SERVER_PERMISSIONS___ + +[ + { + "instance": { + "key": { + "publicId": "read_request", + "versionId": "1" + }, + "param": [ + { + "key": "requestAccess", + "value": { + "type": 1, + "string": "any" + } + }, + { + "key": "headerAccess", + "value": { + "type": 1, + "string": "any" + } + }, + { + "key": "queryParameterAccess", + "value": { + "type": 1, + "string": "any" + } + } + ] + }, + "clientAnnotations": { + "isEditedByUser": true + }, + "isRequired": true + }, + { + "instance": { + "key": { + "publicId": "logging", + "versionId": "1" + }, + "param": [ + { + "key": "environments", + "value": { + "type": 1, + "string": "debug" + } + } + ] + }, + "isRequired": true + } +] + + +___TESTS___ + +scenarios: [] + + +___NOTES___ + +Code generated by sesamy. DO NOT EDIT. +` diff --git a/pkg/tagmanager/server/transformation/mpv2userdata.go b/pkg/tagmanager/server/transformation/mpv2userdata.go new file mode 100644 index 0000000..e56d66f --- /dev/null +++ b/pkg/tagmanager/server/transformation/mpv2userdata.go @@ -0,0 +1,89 @@ +package transformation + +import ( + "google.golang.org/api/tagmanager/v2" +) + +func NewMPv2UserData(name string, variable *tagmanager.Variable, client *tagmanager.Client) *tagmanager.Transformation { + return &tagmanager.Transformation{ + Name: name, + Parameter: []*tagmanager.Parameter{ + { + Key: "matchingConditionsEnabled", + Type: "boolean", + Value: "true", + }, + { + Key: "allTagsExcept", + Type: "boolean", + Value: "true", + }, + { + Key: "booleanExpressionString", + Type: "template", + }, + { + Key: "augmentEventTable", + List: []*tagmanager.Parameter{ + { + IsWeakReference: false, + Map: []*tagmanager.Parameter{ + { + Key: "paramName", + Type: "template", + Value: "user_data", + }, + { + Key: "paramValue", + Type: "template", + Value: "{{" + variable.Name + "}}", + }, + }, + Type: "map", + }, + }, + Type: "list", + }, + { + Key: "affectedTags", + Type: "list", + }, + { + Key: "affectedTagTypes", + Type: "list", + }, + { + Key: "matchingConditionsTable", + List: []*tagmanager.Parameter{ + { + Map: []*tagmanager.Parameter{ + { + Key: "variableName", + Type: "template", + Value: "Client Name", + }, + { + Key: "variableReference", + Type: "template", + Value: "{{Client Name}}", + }, + { + Key: "expressionType", + Type: "template", + Value: "EQUALS", + }, + { + Key: "expressionValue", + Type: "template", + Value: client.Name, + }, + }, + Type: "map", + }, + }, + Type: "list", + }, + }, + Type: "tf_augment_event", + } +} diff --git a/pkg/tagmanager/server/variable/mpv2data.go b/pkg/tagmanager/server/variable/mpv2data.go new file mode 100644 index 0000000..61c3b8f --- /dev/null +++ b/pkg/tagmanager/server/variable/mpv2data.go @@ -0,0 +1,24 @@ +package variable + +import ( + "github.com/foomo/sesamy-cli/pkg/utils" + "google.golang.org/api/tagmanager/v2" +) + +func MPv2DataName(v string) string { + return "mpv2." + v +} + +func NewMPv2Data(name string, template *tagmanager.CustomTemplate) *tagmanager.Variable { + return &tagmanager.Variable{ + Name: name, + Parameter: []*tagmanager.Parameter{ + { + Key: "key", + Type: "template", + Value: name, + }, + }, + Type: utils.TemplateType(template), + } +} diff --git a/pkg/tagmanager/tagmanager.go b/pkg/tagmanager/tagmanager.go index 1e8ca68..f530a08 100644 --- a/pkg/tagmanager/tagmanager.go +++ b/pkg/tagmanager/tagmanager.go @@ -31,6 +31,7 @@ type ( triggers map[string]*tagmanager.Trigger tags map[string]*tagmanager.Tag customTemplates map[string]*tagmanager.CustomTemplate + transformations map[string]*tagmanager.Transformation } Option func(*TagManager) ) @@ -304,6 +305,19 @@ func (t *TagManager) LookupTemplate(name string) (*tagmanager.CustomTemplate, er return elems[name], nil } +func (t *TagManager) LookupTransformation(name string) (*tagmanager.Transformation, error) { + elems, err := t.LoadTransformations() + if err != nil { + return nil, err + } + + if _, ok := elems[name]; !ok { + return nil, ErrNotFound + } + + return elems[name], nil +} + func (t *TagManager) LoadTriggers() (map[string]*tagmanager.Trigger, error) { if t.triggers == nil { t.l.Info("🛄 Loading list", "type", "Trigger") @@ -384,6 +398,24 @@ func (t *TagManager) LoadCustomTemplates() (map[string]*tagmanager.CustomTemplat return t.customTemplates, nil } +func (t *TagManager) LoadTransformations() (map[string]*tagmanager.Transformation, error) { + if t.transformations == nil { + t.l.Info("🛄 Loading list", "type", "Transformation") + r, err := t.Service().Accounts.Containers.Workspaces.Transformations.List(t.WorkspacePath()).Do() + if err != nil { + return nil, err + } + + res := map[string]*tagmanager.Transformation{} + for _, value := range r.Transformation { + res[value.Name] = value + } + t.transformations = res + } + + return t.transformations, nil +} + func (t *TagManager) UpsertClient(item *tagmanager.Client) (*tagmanager.Client, error) { l := t.l.With("type", "Client", "name", item.Name) @@ -419,6 +451,41 @@ func (t *TagManager) UpsertClient(item *tagmanager.Client) (*tagmanager.Client, return t.LookupClient(item.Name) } +func (t *TagManager) UpsertTransformation(item *tagmanager.Transformation) (*tagmanager.Transformation, error) { + l := t.l.With("type", "Transformation", "name", item.Name) + + folder, err := t.LookupFolder(t.folderName) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve folder") + } + item.ParentFolderId = folder.FolderId + + item.Notes = t.Notes(item) + item.AccountId = t.AccountID() + item.ContainerId = t.ContainerID() + item.WorkspaceId = t.WorkspaceID() + + cache, err := t.LookupTransformation(item.Name) + if err != nil && !errors.Is(err, ErrNotFound) { + return nil, err + } + + if cache == nil { + l.Info("🚀 New") + t.transformations[item.Name], err = t.Service().Accounts.Containers.Workspaces.Transformations.Create(t.WorkspacePath(), item).Do() + } else if item.Notes == cache.Notes { + l.Info("✅ OK", "id", cache.TransformationId) + } else { + l.Info("🔄 Update", "id", cache.TransformationId) + t.transformations[item.Name], err = t.Service().Accounts.Containers.Workspaces.Transformations.Update(t.WorkspacePath()+"/transformations/"+cache.TransformationId, item).Do() + } + if err != nil { + return nil, err + } + + return t.LookupTransformation(item.Name) +} + func (t *TagManager) UpsertFolder(name string) (*tagmanager.Folder, error) { l := t.l.With("type", "Folder", "name", name) diff --git a/sesamy.schema.json b/sesamy.schema.json index 421f568..433ef1a 100644 --- a/sesamy.schema.json +++ b/sesamy.schema.json @@ -220,13 +220,39 @@ "description": "Google Consent settings" }, "serverContainer": { - "$ref": "#/$defs/github.com.foomo.gocontemplate.pkg.contemplate.Config", + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.FacebookServerContainer", "description": "Google Tag Manager server container settings" } }, "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.FacebookConversionAPITag": { + "properties": { + "extendCookies": { + "type": "boolean", + "description": "Extend Meta Pixel cookies (fbp/fbc)" + }, + "enableEventEnhancement": { + "type": "boolean", + "description": "Enable Use of HTTP Only Secure Cookie (gtmeec) to Enhance Event Data" + } + }, + "additionalProperties": false, + "type": "object" + }, + "github.com.foomo.sesamy-cli.pkg.config.FacebookServerContainer": { + "properties": { + "packages": { + "$ref": "#/$defs/[]*contemplate.PackageConfig" + }, + "settings": { + "$ref": "#/$defs/map[string]config.FacebookConversionAPITag" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.GoogleAPI": { "properties": { "credentials": { @@ -275,13 +301,25 @@ "description": "Enable provider" }, "serverContainer": { - "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.ServerContainer", + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleAdsConversionServerContainer", "description": "Google Tag Manager server container settings" } }, "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.GoogleAdsConversionServerContainer": { + "properties": { + "packages": { + "$ref": "#/$defs/[]*contemplate.PackageConfig" + }, + "settings": { + "$ref": "#/$defs/map[string]config.GoogleAdsConversionTracking" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.GoogleAdsConversionTracking": { "properties": { "label": { @@ -483,18 +521,6 @@ "additionalProperties": false, "type": "object" }, - "github.com.foomo.sesamy-cli.pkg.config.ServerContainer": { - "properties": { - "packages": { - "$ref": "#/$defs/[]*contemplate.PackageConfig" - }, - "settings": { - "$ref": "#/$defs/map[string]config.GoogleAdsConversionTracking" - } - }, - "additionalProperties": false, - "type": "object" - }, "github.com.foomo.sesamy-cli.pkg.config.Tracify": { "properties": { "enabled": { @@ -509,6 +535,10 @@ "type": "string", "description": "Tracify customer site id" }, + "stagingModeEnabled": { + "type": "boolean", + "description": "Enable stating mode" + }, "googleConsent": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent", "description": "Google Consent settings" @@ -560,6 +590,12 @@ "additionalProperties": false, "type": "object" }, + "map[string]config.FacebookConversionAPITag": { + "additionalProperties": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.FacebookConversionAPITag" + }, + "type": "object" + }, "map[string]config.GoogleAdsConversionTracking": { "additionalProperties": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleAdsConversionTracking" diff --git a/test/tagmanager/clientserver_test.go b/test/tagmanager/clientserver_test.go index 6d19310..d37e896 100644 --- a/test/tagmanager/clientserver_test.go +++ b/test/tagmanager/clientserver_test.go @@ -94,6 +94,15 @@ func TestNewClient_Server(t *testing.T) { }) } + { // --- Transformations --- + t.Run("list transformations", func(t *testing.T) { + cmd := c.Service().Accounts.Containers.Workspaces.Transformations.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + } + { // --- Tags --- t.Run("list tags", func(t *testing.T) { cmd := c.Service().Accounts.Containers.Workspaces.Tags.List(c.WorkspacePath()) @@ -113,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[3].TemplateData) + fmt.Println(r.Template[8].TemplateData) } }) }