From cfef82ee2ddf372e96b9e3de5afe68c9bf49be7e Mon Sep 17 00:00:00 2001 From: Kevin Franklin Kim Date: Wed, 16 Sep 2026 08:03:31 +0200 Subject: [PATCH] feat: support custom templates across multiple providers Add the ability to specify custom template paths for various providers (e.g., Pinterest, Facebook, Umami, and more). Includes logic for resolving and provisioning templates, defaulting to embedded ones when not specified. Updated related documentation. --- README.md | 52 ++++- docs/CONTRIBUTING.md | 15 +- docs/providers/cookiebot.md | 3 + docs/providers/criteo.md | 5 + docs/providers/emarsys.md | 7 + docs/providers/facebook.md | 3 + docs/providers/google-analytics.md | 5 + docs/providers/microsoft-ads.md | 3 + docs/providers/mixpanel.md | 19 +- docs/providers/pinterest.md | 3 + docs/providers/tracify.md | 3 + docs/providers/umami.md | 3 + docs/providers/utm.md | 5 + pkg/config/cookiebot.go | 7 + pkg/config/criteo.go | 9 + pkg/config/emarsys.go | 11 ++ pkg/config/facebook.go | 6 + pkg/config/googleanalytics.go | 9 + pkg/config/microsoftads.go | 7 + pkg/config/mixpanel.go | 8 +- pkg/config/pinterest.go | 7 + pkg/config/tracify.go | 7 + pkg/config/umami.go | 7 + pkg/config/utm.go | 9 + pkg/provider/cookiebot/web.go | 6 +- pkg/provider/criteo/server.go | 6 +- pkg/provider/criteo/web.go | 6 +- pkg/provider/emarsys/server.go | 15 +- pkg/provider/emarsys/web.go | 8 +- pkg/provider/facebook/server.go | 5 +- pkg/provider/googleanalytics/server.go | 15 +- pkg/provider/microsoftads/server.go | 8 +- pkg/provider/mixpanel/server.go | 20 +- pkg/provider/pinterest/server.go | 6 +- pkg/provider/tracify/server.go | 8 +- pkg/provider/umami/server.go | 8 +- pkg/provider/utm/server.go | 15 +- .../common/template/customtemplate.go | 57 ++++++ .../common/template/customtemplate_test.go | 153 +++++++++++++++ pkg/tagmanager/tagmanager.go | 4 +- sesamy.schema.json | 178 +++++++++++++++++- 41 files changed, 683 insertions(+), 48 deletions(-) create mode 100644 pkg/tagmanager/common/template/customtemplate.go create mode 100644 pkg/tagmanager/common/template/customtemplate_test.go diff --git a/README.md b/README.md index 30eb495..4a5191a 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,12 @@ googleTag: googleAnalytics: # Enable provider enabled: true + # Custom tag template settings + templates: + # Path to a custom json request value variable template file, defaults to the embedded one + jsonRequestValueVariable: '' + # Path to a custom gtag client template file, defaults to the embedded one + gtagClient: '' # Google Consent settings googleConsent: # Enable consent mode @@ -294,6 +300,10 @@ umami: websiteId: '' # Endpoint url of the umami api endpointUrl: https://umami.your-domain.com + # Custom tag template settings + templates: + # Path to a custom tag template file, defaults to the embedded one + tag: '' # Google Consent settings googleConsent: # Enable consent mode @@ -320,6 +330,12 @@ criteo: partnerId: 123456 # Criteo applicaiton id applicationId: com.foomo + # Custom tag template settings + templates: + # Path to a custom events api tag template file, defaults to the manually installed one + eventsApiTag: '' + # Path to a custom user identification template file, defaults to the manually installed one + userIdentification: '' # Google Tag Manager server container settings serverContainer: # Path to the go.mod file @@ -347,6 +363,10 @@ facebook: apiAccessToken: '' # Code used to verify that your server events are received correctly by Conversions API testEventToken: '' + # Custom tag template settings + templates: + # Path to a custom conversions api tag template file, defaults to the manually installed one + conversionsApiTag: '' # Google Tag Manager server container settings # Google Consent settings googleConsent: @@ -377,6 +397,14 @@ emarsys: enabled: true # Emarsys merchant id merchantId: '' + # Custom tag template settings + templates: + # Path to a custom web extend tag template file, defaults to the embedded one + webExtendTag: '' + # Path to a custom initialization client template file, defaults to the embedded one + initializationClient: '' + # Path to a custom initialization tag template file, defaults to the embedded one + initializationTag: '' # Google Consent settings googleConsent: # Enable consent mode @@ -403,6 +431,10 @@ tracify: token: '' # Tracify customer site id customerSiteId: '' + # Custom tag template settings + templates: + # Path to a custom tag template file, defaults to the embedded one + tag: '' # Google Consent settings googleConsent: # Enable consent mode @@ -436,6 +468,10 @@ cookiebot: # Name of the manually installed Cookiebot CMP tag template # "https://tagmanager.google.com/gallery/#/owners/cybotcorp/templates/gtm-templates-cookiebot-cmp templateName: Cookiebot CMP + # Custom tag template settings + templates: + # Path to a custom cmp tag template file, defaults to the manually installed one + tag: '' # Cookiebot id cookiebotId: '' # CDN Region (eu, com) @@ -463,6 +499,12 @@ cookiebot: utm: # Enable provider enabled: true + # Custom tag template settings + templates: + # Path to a custom attribution variable template file, defaults to the embedded one + attributionVariable: '' + # Path to a custom cookie writer tag template file, defaults to the embedded one + cookieWriterTag: '' # Google Consent settings googleConsent: # Enable consent mode @@ -476,8 +518,10 @@ mixpanel: enabled: true # Project Token projectToken: '' - # Lookup the manually installed "Mixpanel" tag template instead of provisioning it - lookupTagTemplate: false + # Custom tag template settings + templates: + # Path to a custom tag template file, defaults to the embedded one + tag: '' # Google Consent settings googleConsent: # Enable consent mode @@ -517,6 +561,10 @@ pinterest: apiAccessToken: '' # Enable test mode testModeEnabled: false + # Custom tag template settings + templates: + # Path to a custom conversions tag template file, defaults to the manually installed one + tag: '' # Google Consent settings googleConsent: # Enable consent mode diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6958aa2..d0d4657 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -8,8 +8,8 @@ Thank you for your interest in contributing to our project! This guide will help 2. Create a new branch: `git checkout -b feature/your-feature-name` 3. Make your changes 4. Check and fix code style and formatting issues: `make lint` -5. Run checks: `make test` -6. Build the project: `make build` +5. Run tests: `make test` +6. Run final checks: `make check` 7. Commit your changes using the conventions below 8. Push your branch to your fork 9. Open a pull request @@ -29,10 +29,13 @@ We follow [Conventional Commits](https://www.conventionalcommits.org/) for clear ## Pull Request Guidelines -1. Update documentation if needed -2. Ensure all tests pass -3. Address any feedback from code reviews -4. Once approved, your PR will be merged +1. Adhere to the coding and style guidelines of the project +2. Perform a self-review of your own code +3. Comment your code, particularly in hard-to-understand areas +4. Make corresponding changes to the documentation +5. Ensure all tests pass +6. Address any feedback from code reviews +7. Once approved, your PR will be merged ## Code of Conduct & Security diff --git a/docs/providers/cookiebot.md b/docs/providers/cookiebot.md index 87b5cc4..e033963 100644 --- a/docs/providers/cookiebot.md +++ b/docs/providers/cookiebot.md @@ -8,6 +8,8 @@ Cookiebot CMP integration. The CLI does **not** install the Cookiebot tag templa cookiebot: enabled: true templateName: Cookiebot CMP # must match the imported template's name + templates: + tag: '' cookiebotId: '' cdnRegion: eu # eu or com urlPassthrough: false @@ -29,3 +31,4 @@ cookiebot: | `urlPassthrough` | Pass identifiers via URL parameters when consent is denied. | | `advertiserConsentModeEnabled` | Enable advertiser consent mode signals. | | `regionSettings[]` | Per-region default consent states. Leave `region` blank for the global default. | +| `templates.tag` | Path to a custom CMP tag template file. Empty (default) looks up the manually installed template. Overrides `templateName` lookup. | diff --git a/docs/providers/criteo.md b/docs/providers/criteo.md index da08c7d..62f1e62 100644 --- a/docs/providers/criteo.md +++ b/docs/providers/criteo.md @@ -10,6 +10,9 @@ criteo: callerId: 123 partnerId: 123456 applicationId: com.foomo + templates: + eventsApiTag: '' + userIdentification: '' serverContainer: directory: . packages: @@ -30,3 +33,5 @@ criteo: | `partnerId` | Criteo partner ID. | | `applicationId` | Application identifier (often the reversed-domain form). | | `serverContainer.packages` | Events to forward to Criteo. | +| `templates.eventsApiTag` | Path to a custom Events API tag template file. Empty (default) looks up the manually installed template. | +| `templates.userIdentification` | Path to a custom User Identification template file. Empty (default) looks up the manually installed template. | diff --git a/docs/providers/emarsys.md b/docs/providers/emarsys.md index 16607bb..6b279e7 100644 --- a/docs/providers/emarsys.md +++ b/docs/providers/emarsys.md @@ -8,6 +8,10 @@ Emarsys server-side integration. emarsys: enabled: true merchantId: '' + templates: + webExtendTag: '' + initializationClient: '' + initializationTag: '' googleConsent: enabled: true mode: analytics_storage @@ -26,3 +30,6 @@ emarsys: | `merchantId` | Emarsys merchant ID. | | `googleConsent.mode` | Defaults to `analytics_storage`. | | `serverContainer.packages` | Events to forward to Emarsys. | +| `templates.webExtendTag` | Path to a custom web extend tag template file. Empty (default) provisions the embedded template. | +| `templates.initializationClient` | Path to a custom initialization client template file. Empty (default) provisions the embedded template. | +| `templates.initializationTag` | Path to a custom initialization tag template file. Empty (default) provisions the embedded template. | diff --git a/docs/providers/facebook.md b/docs/providers/facebook.md index d14af7c..c6ac9fa 100644 --- a/docs/providers/facebook.md +++ b/docs/providers/facebook.md @@ -10,6 +10,8 @@ facebook: pixelId: '' apiAccessToken: '' testEventToken: '' + templates: + conversionsApiTag: '' googleConsent: enabled: true mode: ad_storage @@ -36,6 +38,7 @@ facebook: | `testEventToken` | Optional — use Meta's test events tool to verify the payload. | | `googleConsent.mode` | Defaults to `ad_storage`. | | `serverContainer.packages` | Events to send via Conversions API. | +| `templates.conversionsApiTag` | Path to a custom Conversions API tag template file. Empty (default) looks up the manually installed template. | ## Test events diff --git a/docs/providers/google-analytics.md b/docs/providers/google-analytics.md index 5c14102..2e4e948 100644 --- a/docs/providers/google-analytics.md +++ b/docs/providers/google-analytics.md @@ -8,6 +8,9 @@ GA4 web + server-side tagging. googleAnalytics: enabled: true propertyId: '' # used by `sesamy open ga` + templates: + jsonRequestValueVariable: '' + gtagClient: '' googleConsent: enabled: true mode: analytics_storage @@ -41,3 +44,5 @@ googleAnalytics: | `googleGTagJSOverride.ecommerceItems` | Normalize ecommerce items in the override. | | `webContainer.packages` | Events handled in the web container. | | `serverContainer.packages` | Events handled in the server container. | +| `templates.jsonRequestValueVariable` | Path to a custom JSON request value variable template file. Empty (default) provisions the embedded template. | +| `templates.gtagClient` | Path to a custom gtag client template file. Empty (default) provisions the embedded template. | diff --git a/docs/providers/microsoft-ads.md b/docs/providers/microsoft-ads.md index 2a5ae6f..919688c 100644 --- a/docs/providers/microsoft-ads.md +++ b/docs/providers/microsoft-ads.md @@ -8,6 +8,8 @@ Microsoft Ads (Bing UET) server-side conversion tracking. microsoftAds: enabled: true tagId: '' + templates: + conversionTag: '' googleConsent: enabled: true mode: ad_storage @@ -30,6 +32,7 @@ microsoftAds: | `googleConsent.mode` | Defaults to `ad_storage`. | | `conversion.serverContainer.settings.` | List of `{ conversionLabel }` pairs — one tag per entry. | | `conversion.serverContainer.packages` | Events to wire conversion tags for. | +| `templates.conversionTag` | Path to a custom conversion tag template file. Empty (default) provisions the embedded template. | ::: tip Field names are based on the current schema. Run `sesamy config -c sesamy.yaml` to see the resolved values your build will use. diff --git a/docs/providers/mixpanel.md b/docs/providers/mixpanel.md index 45d1a7a..02e85d9 100644 --- a/docs/providers/mixpanel.md +++ b/docs/providers/mixpanel.md @@ -10,7 +10,8 @@ Track tags send UTM attribution data, so the [UTM](./utm) provider must be enabl mixpanel: enabled: true projectToken: '' - lookupTagTemplate: false + templates: + tag: '' googleConsent: enabled: true mode: analytics_storage @@ -38,7 +39,7 @@ mixpanel: | Field | Purpose | |-------|---------| | `projectToken` | Mixpanel project token. | -| `lookupTagTemplate` | When `true`, look up an existing `Mixpanel` tag template instead of provisioning the built-in one. Defaults to `false`. | +| `templates.tag` | Path to a custom tag template file. Empty (default) provisions the embedded template. | | `googleConsent.mode` | Defaults to `analytics_storage`. | | `serverContainer.userId` | Event-data property holding the user ID, forwarded to Mixpanel. | | `serverContainer.track.packages` | Events to forward to Mixpanel's `/track` endpoint. | @@ -47,6 +48,14 @@ mixpanel: By default the `Mixpanel` tag template is provisioned by the CLI — no manual template install required. -Set `lookupTagTemplate: true` to use a template you manage yourself instead. The CLI then looks up a -template named `Mixpanel` in the server container and fails if it is missing — install the -[Mixpanel](https://github.com/stape-io/mixpanel-tag) template by stape-io from the GTM gallery first. +Set `templates.tag` to the path of a template file to provision your own instead: + +```yaml +mixpanel: + templates: + tag: ./templates/mixpanel.tpl +``` + +The file is uploaded verbatim as the template named `Mixpanel`, so it must be a complete GTM template +(`___INFO___`, `___TEMPLATE_PARAMETERS___`, `___SANDBOXED_JS_FOR_SERVER___`, …). Use the +[Mixpanel](https://github.com/stape-io/mixpanel-tag) template by stape-io as a starting point. diff --git a/docs/providers/pinterest.md b/docs/providers/pinterest.md index 6235fca..8bf568a 100644 --- a/docs/providers/pinterest.md +++ b/docs/providers/pinterest.md @@ -10,6 +10,8 @@ pinterest: advertiserId: '' apiAccessToken: '' testModeEnabled: false + templates: + tag: '' googleConsent: enabled: true mode: analytics_storage @@ -34,3 +36,4 @@ pinterest: | `testModeEnabled` | Send events with the `test` flag set — won't count toward live data. | | `googleConsent.mode` | Defaults to `analytics_storage`. | | `serverContainer.packages` | Events to send. | +| `templates.tag` | Path to a custom conversions tag template file. Empty (default) looks up the manually installed template. | diff --git a/docs/providers/tracify.md b/docs/providers/tracify.md index 93656fb..2aa95ee 100644 --- a/docs/providers/tracify.md +++ b/docs/providers/tracify.md @@ -9,6 +9,8 @@ tracify: enabled: true token: '' customerSiteId: '' + templates: + tag: '' googleConsent: enabled: true mode: analytics_storage @@ -29,3 +31,4 @@ tracify: | `customerSiteId` | Tracify customer site identifier. | | `googleConsent.mode` | Defaults to `analytics_storage`. | | `serverContainer.packages` | Events to forward. | +| `templates.tag` | Path to a custom tag template file. Empty (default) provisions the embedded template. | diff --git a/docs/providers/umami.md b/docs/providers/umami.md index 0329f08..42b6a4c 100644 --- a/docs/providers/umami.md +++ b/docs/providers/umami.md @@ -10,6 +10,8 @@ umami: domain: your-domain.com websiteId: '' endpointUrl: https://umami.your-domain.com + templates: + tag: '' googleConsent: enabled: true mode: analytics_storage @@ -29,3 +31,4 @@ umami: | `endpointUrl` | Base URL of your Umami instance. | | `googleConsent.mode` | Defaults to `analytics_storage`. | | `serverContainer.packages` | Events to forward. | +| `templates.tag` | Path to a custom tag template file. Empty (default) provisions the embedded template. | diff --git a/docs/providers/utm.md b/docs/providers/utm.md index 16cf67d..e7a5e15 100644 --- a/docs/providers/utm.md +++ b/docs/providers/utm.md @@ -7,6 +7,9 @@ UTM attribution for the server container. Persists the incoming UTM parameters i ```yaml utm: enabled: true + templates: + attributionVariable: '' + cookieWriterTag: '' googleConsent: enabled: true mode: ad_storage @@ -15,6 +18,8 @@ utm: | Field | Purpose | |-------|---------| | `googleConsent.mode` | Defaults to `ad_storage`. | +| `templates.attributionVariable` | Path to a custom attribution variable template file. Empty (default) provisions the embedded template. | +| `templates.cookieWriterTag` | Path to a custom cookie writer tag template file. Empty (default) provisions the embedded template. | ## What gets provisioned diff --git a/pkg/config/cookiebot.go b/pkg/config/cookiebot.go index 977da16..8952893 100644 --- a/pkg/config/cookiebot.go +++ b/pkg/config/cookiebot.go @@ -15,4 +15,11 @@ type Cookiebot struct { AdvertiserConsentModeEnabled bool `json:"advertiserConsentModeEnabled" yaml:"advertiserConsentModeEnabled"` // Default Consent state RegionSettings []CookiebotRegionSetting `json:"regionSettings" yaml:"regionSettings"` + // Custom tag template settings + Templates CookiebotTemplates `json:"templates" yaml:"templates"` +} + +type CookiebotTemplates struct { + // Path to a custom cmp tag template file, defaults to the manually installed one + Tag string `json:"tag" yaml:"tag"` } diff --git a/pkg/config/criteo.go b/pkg/config/criteo.go index f941f4e..f6200c5 100644 --- a/pkg/config/criteo.go +++ b/pkg/config/criteo.go @@ -13,8 +13,17 @@ type Criteo struct { PartnerID string `json:"partnerId" yaml:"partnerId"` // Criteo application id ApplicationID string `json:"applicationId" yaml:"applicationId"` + // Custom tag template settings + Templates CriteoTemplates `json:"templates" yaml:"templates"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` // Google Tag Manager server container settings ServerContainer contemplate.Config `json:"serverContainer" yaml:"serverContainer"` } + +type CriteoTemplates struct { + // Path to a custom events api tag template file, defaults to the manually installed one + EventsAPITag string `json:"eventsApiTag" yaml:"eventsApiTag"` + // Path to a custom user identification template file, defaults to the manually installed one + UserIdentification string `json:"userIdentification" yaml:"userIdentification"` +} diff --git a/pkg/config/emarsys.go b/pkg/config/emarsys.go index ea6901d..6e727fe 100644 --- a/pkg/config/emarsys.go +++ b/pkg/config/emarsys.go @@ -13,6 +13,8 @@ type Emarsys struct { TestMode bool `json:"testMode" yaml:"testMode"` // Enable debug mode DebugMode bool `json:"debugMode" yaml:"debugMode"` + // Custom tag template settings + Templates EmarsysTemplates `json:"templates" yaml:"templates"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` // Google Tag Manager web container settings @@ -20,3 +22,12 @@ type Emarsys struct { // Google Tag Manager server container settings ServerContainer contemplate.Config `json:"serverContainer" yaml:"serverContainer"` } + +type EmarsysTemplates struct { + // Path to a custom web extend tag template file, defaults to the embedded one + WebExtendTag string `json:"webExtendTag" yaml:"webExtendTag"` + // Path to a custom initialization client template file, defaults to the embedded one + InitializationClient string `json:"initializationClient" yaml:"initializationClient"` + // Path to a custom initialization tag template file, defaults to the embedded one + InitializationTag string `json:"initializationTag" yaml:"initializationTag"` +} diff --git a/pkg/config/facebook.go b/pkg/config/facebook.go index 82dc449..318190b 100644 --- a/pkg/config/facebook.go +++ b/pkg/config/facebook.go @@ -11,6 +11,8 @@ type ( PixelID string `json:"pixelId" yaml:"pixelId"` APIAccessToken string `json:"apiAccessToken" yaml:"apiAccessToken"` TestEventToken string `json:"testEventToken" yaml:"testEventToken"` + // Custom tag template settings + Templates FacebookTemplates `json:"templates" yaml:"templates"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` // Google Tag Manager server container settings @@ -20,6 +22,10 @@ type ( contemplate.Config `json:",inline" yaml:",squash"` Settings map[string]FacebookConversionAPITag `json:"settings" yaml:"settings"` } + FacebookTemplates struct { + // Path to a custom conversions api tag template file, defaults to the manually installed one + ConversionsAPITag string `json:"conversionsApiTag" yaml:"conversionsApiTag"` + } ) func (s *FacebookServerContainer) Setting(eventName string) FacebookConversionAPITag { diff --git a/pkg/config/googleanalytics.go b/pkg/config/googleanalytics.go index 7131c82..491b315 100644 --- a/pkg/config/googleanalytics.go +++ b/pkg/config/googleanalytics.go @@ -17,8 +17,17 @@ type GoogleAnalytics struct { GoogleGTagJSOverride GoogleAnalyticsGTagJSOverride `json:"googleGTagJSOverride" yaml:"googleGTagJSOverride"` // Enable mpv2 user data transformation (experimental) EnableMPv2UserDataTransformation bool `json:"enableMPv2UserDataTransformation" yaml:"enableMPv2UserDataTransformation"` + // Custom tag template settings + Templates GoogleAnalyticsTemplates `json:"templates" yaml:"templates"` // Google Tag Manager web container settings WebContainer contemplate.Config `json:"webContainer" yaml:"webContainer"` // Google Tag Manager server container settings ServerContainer contemplate.Config `json:"serverContainer" yaml:"serverContainer"` } + +type GoogleAnalyticsTemplates struct { + // Path to a custom json request value variable template file, defaults to the embedded one + JSONRequestValueVariable string `json:"jsonRequestValueVariable" yaml:"jsonRequestValueVariable"` + // Path to a custom gtag client template file, defaults to the embedded one + GTagClient string `json:"gtagClient" yaml:"gtagClient"` +} diff --git a/pkg/config/microsoftads.go b/pkg/config/microsoftads.go index ade76de..c02fb95 100644 --- a/pkg/config/microsoftads.go +++ b/pkg/config/microsoftads.go @@ -9,4 +9,11 @@ type MicrosoftAds struct { TagID string `json:"tagId" yaml:"tagId"` // Microsoft Ads Conversion settings Conversion MicrosoftAdsConversion `json:"conversion" yaml:"conversion"` + // Custom tag template settings + Templates MicrosoftAdsTemplates `json:"templates" yaml:"templates"` +} + +type MicrosoftAdsTemplates struct { + // Path to a custom conversion tag template file, defaults to the embedded one + ConversionTag string `json:"conversionTag" yaml:"conversionTag"` } diff --git a/pkg/config/mixpanel.go b/pkg/config/mixpanel.go index 1c8b16f..4e42200 100644 --- a/pkg/config/mixpanel.go +++ b/pkg/config/mixpanel.go @@ -10,12 +10,16 @@ type ( Enabled bool `json:"enabled" yaml:"enabled"` // Mixpanel project token ProjectToken string `json:"projectToken" yaml:"projectToken"` - // Lookup tag template instead of provisioning it - LookupTagTemplate bool `json:"lookupTagTemplate" yaml:"lookupTagTemplate"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` // Google Tag Manager server container settings ServerContainer MixpanelServerContainer `json:"serverContainer" yaml:"serverContainer"` + // Custom tag template settings + Templates MixpanelTemplates `json:"templates" yaml:"templates"` + } + MixpanelTemplates struct { + // Path to a custom tag template file, defaults to the embedded one + Tag string `json:"tag" yaml:"tag"` } MixpanelServerContainer struct { UserID string `json:"userId" yaml:"userId"` diff --git a/pkg/config/pinterest.go b/pkg/config/pinterest.go index ef0e648..3871b2e 100644 --- a/pkg/config/pinterest.go +++ b/pkg/config/pinterest.go @@ -13,8 +13,15 @@ type Pinterest struct { APIAccessToken string `json:"apiAccessToken" yaml:"apiAccessToken"` // Enable test mode TestModeEnabled bool `json:"testModeEnabled" yaml:"testModeEnabled"` + // Custom tag template settings + Templates PinterestTemplates `json:"templates" yaml:"templates"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` // Google Tag Manager server container settings ServerContainer contemplate.Config `json:"serverContainer" yaml:"serverContainer"` } + +type PinterestTemplates struct { + // Path to a custom conversions tag template file, defaults to the manually installed one + Tag string `json:"tag" yaml:"tag"` +} diff --git a/pkg/config/tracify.go b/pkg/config/tracify.go index 42e948f..fcc8066 100644 --- a/pkg/config/tracify.go +++ b/pkg/config/tracify.go @@ -13,8 +13,15 @@ type Tracify struct { CustomerSiteID string `json:"customerSiteId" yaml:"customerSiteId"` // Enable stating mode StagingModeEnabled bool `json:"stagingModeEnabled" yaml:"stagingModeEnabled"` + // Custom tag template settings + Templates TracifyTemplates `json:"templates" yaml:"templates"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` // Google Tag Manager server container settings ServerContainer contemplate.Config `json:"serverContainer" yaml:"serverContainer"` } + +type TracifyTemplates struct { + // Path to a custom tag template file, defaults to the embedded one + Tag string `json:"tag" yaml:"tag"` +} diff --git a/pkg/config/umami.go b/pkg/config/umami.go index c488f94..63d56ce 100644 --- a/pkg/config/umami.go +++ b/pkg/config/umami.go @@ -10,8 +10,15 @@ type Umami struct { Domain string `json:"domain" yaml:"domain"` WebsiteID string `json:"websiteId" yaml:"websiteId"` EndpointURL string `json:"endpointUrl" yaml:"endpointUrl"` + // Custom tag template settings + Templates UmamiTemplates `json:"templates" yaml:"templates"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` // Google Tag Manager server container settings ServerContainer contemplate.Config `json:"serverContainer" yaml:"serverContainer"` } + +type UmamiTemplates struct { + // Path to a custom tag template file, defaults to the embedded one + Tag string `json:"tag" yaml:"tag"` +} diff --git a/pkg/config/utm.go b/pkg/config/utm.go index b500efb..396d8fc 100644 --- a/pkg/config/utm.go +++ b/pkg/config/utm.go @@ -3,6 +3,15 @@ package config type Utm struct { // Enable provider Enabled bool `json:"enabled" yaml:"enabled"` + // Custom tag template settings + Templates UtmTemplates `json:"templates" yaml:"templates"` // Google Consent settings GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` } + +type UtmTemplates struct { + // Path to a custom attribution variable template file, defaults to the embedded one + AttributionVariable string `json:"attributionVariable" yaml:"attributionVariable"` + // Path to a custom cookie writer tag template file, defaults to the embedded one + CookieWriterTag string `json:"cookieWriterTag" yaml:"cookieWriterTag"` +} diff --git a/pkg/provider/cookiebot/web.go b/pkg/provider/cookiebot/web.go index 1f3b961..38531f2 100644 --- a/pkg/provider/cookiebot/web.go +++ b/pkg/provider/cookiebot/web.go @@ -6,7 +6,9 @@ import ( "github.com/foomo/sesamy-cli/pkg/config" "github.com/foomo/sesamy-cli/pkg/provider/cookiebot/web/tag" "github.com/foomo/sesamy-cli/pkg/tagmanager" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" "github.com/pkg/errors" + tagmanager2 "google.golang.org/api/tagmanager/v2" ) func Web(ctx context.Context, tm *tagmanager.TagManager, cfg config.Cookiebot) error { @@ -16,7 +18,9 @@ func Web(ctx context.Context, tm *tagmanager.TagManager, cfg config.Cookiebot) e } { // create event tags - temmplate, err := tm.LookupTemplate(ctx, cfg.TemplateName) + temmplate, err := commontemplate.ResolveOrLookup(ctx, tm, cfg.Templates.Tag, cfg.TemplateName, func() (*tagmanager2.CustomTemplate, error) { + return tm.LookupTemplate(ctx, cfg.TemplateName) + }) if err != nil { return errors.Wrapf(err, "Failed to lookup `%s`, please install the `%s` gallery tag template first (%s)", cfg.TemplateName, "Cookiebot CMP", "https://tagmanager.google.com/gallery/#/owners/cybotcorp/templates/gtm-templates-cookiebot-cmp") } diff --git a/pkg/provider/criteo/server.go b/pkg/provider/criteo/server.go index 7580cd3..277fb74 100644 --- a/pkg/provider/criteo/server.go +++ b/pkg/provider/criteo/server.go @@ -10,9 +10,11 @@ import ( "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" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" commonvariable "github.com/foomo/sesamy-cli/pkg/tagmanager/common/variable" "github.com/foomo/sesamy-cli/pkg/utils" "github.com/pkg/errors" + tagmanager2 "google.golang.org/api/tagmanager/v2" ) func Server(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg config.Criteo) error { @@ -21,7 +23,9 @@ func Server(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg return err } - template, err := tm.LookupTemplate(ctx, NameCriteoEventsAPITemplate) + template, err := commontemplate.ResolveOrLookup(ctx, tm, cfg.Templates.EventsAPITag, NameCriteoEventsAPITemplate, func() (*tagmanager2.CustomTemplate, error) { + return tm.LookupTemplate(ctx, NameCriteoEventsAPITemplate) + }) if err != nil { if errors.Is(err, tagmanager.ErrNotFound) { l.Warn("Please install the 'Criteo Events API' Tag Template manually first") diff --git a/pkg/provider/criteo/web.go b/pkg/provider/criteo/web.go index 773974f..05ed3c9 100644 --- a/pkg/provider/criteo/web.go +++ b/pkg/provider/criteo/web.go @@ -8,7 +8,9 @@ import ( "github.com/foomo/sesamy-cli/pkg/config" client "github.com/foomo/sesamy-cli/pkg/provider/criteo/web/tag" "github.com/foomo/sesamy-cli/pkg/tagmanager" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" commonvariable "github.com/foomo/sesamy-cli/pkg/tagmanager/common/variable" + tagmanager2 "google.golang.org/api/tagmanager/v2" ) func Web(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg config.Criteo) error { @@ -17,7 +19,9 @@ func Web(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg con return err } - template, err := tm.LookupTemplate(ctx, NameCriteoUserIdentificationTemplate) + template, err := commontemplate.ResolveOrLookup(ctx, tm, cfg.Templates.UserIdentification, NameCriteoUserIdentificationTemplate, func() (*tagmanager2.CustomTemplate, error) { + return tm.LookupTemplate(ctx, NameCriteoUserIdentificationTemplate) + }) if err != nil { if errors.Is(err, tagmanager.ErrNotFound) { l.Warn("Please install the 'Criteo User Identification' template manually first") diff --git a/pkg/provider/emarsys/server.go b/pkg/provider/emarsys/server.go index e54dc0c..e7e3e27 100644 --- a/pkg/provider/emarsys/server.go +++ b/pkg/provider/emarsys/server.go @@ -12,6 +12,7 @@ import ( "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" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" commonvariable "github.com/foomo/sesamy-cli/pkg/tagmanager/common/variable" "github.com/foomo/sesamy-cli/pkg/utils" "github.com/pkg/errors" @@ -29,12 +30,22 @@ func Server(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg return err } - tagTemplate, err := tm.UpsertCustomTemplate(ctx, template.NewEmarsysWebExtendTag(NameServerEmarsysWebExtendTagTemplate)) + webExtendTemplate, err := commontemplate.Resolve(cfg.Templates.WebExtendTag, NameServerEmarsysWebExtendTagTemplate, template.NewEmarsysWebExtendTag(NameServerEmarsysWebExtendTagTemplate)) if err != nil { return err } - clientTemplate, err := tm.UpsertCustomTemplate(ctx, template.NewEmarsysInitializationClient(NameServerEmarsysInitalizationClientTemplate)) + tagTemplate, err := tm.UpsertCustomTemplate(ctx, webExtendTemplate) + if err != nil { + return err + } + + initializationClientTemplate, err := commontemplate.Resolve(cfg.Templates.InitializationClient, NameServerEmarsysInitalizationClientTemplate, template.NewEmarsysInitializationClient(NameServerEmarsysInitalizationClientTemplate)) + if err != nil { + return err + } + + clientTemplate, err := tm.UpsertCustomTemplate(ctx, initializationClientTemplate) if err != nil { return err } diff --git a/pkg/provider/emarsys/web.go b/pkg/provider/emarsys/web.go index 4dac16a..65b35d8 100644 --- a/pkg/provider/emarsys/web.go +++ b/pkg/provider/emarsys/web.go @@ -10,6 +10,7 @@ import ( "github.com/foomo/sesamy-cli/pkg/provider/googletag" commonvariable "github.com/foomo/sesamy-cli/pkg/provider/googletag/web/variable" "github.com/foomo/sesamy-cli/pkg/tagmanager" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" commontrigger "github.com/foomo/sesamy-cli/pkg/tagmanager/common/trigger" "github.com/pkg/errors" ) @@ -21,7 +22,12 @@ func Web(ctx context.Context, tm *tagmanager.TagManager, cfg config.Emarsys) err } { // create initialization tag - tagTemplate, err := tm.UpsertCustomTemplate(ctx, template.NewEmarsysInitializationTag(NameWebEmarsysInitalizationTagTemplate)) + initializationTemplate, err := commontemplate.Resolve(cfg.Templates.InitializationTag, NameWebEmarsysInitalizationTagTemplate, template.NewEmarsysInitializationTag(NameWebEmarsysInitalizationTagTemplate)) + if err != nil { + return err + } + + tagTemplate, err := tm.UpsertCustomTemplate(ctx, initializationTemplate) if err != nil { return err } diff --git a/pkg/provider/facebook/server.go b/pkg/provider/facebook/server.go index c7781a8..c2b29a8 100644 --- a/pkg/provider/facebook/server.go +++ b/pkg/provider/facebook/server.go @@ -10,6 +10,7 @@ import ( "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" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" commonvariable "github.com/foomo/sesamy-cli/pkg/tagmanager/common/variable" "github.com/foomo/sesamy-cli/pkg/utils" "github.com/pkg/errors" @@ -43,7 +44,9 @@ func Server(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg } } - template, err := tm.LookupTemplate(ctx, NameConversionsAPITagTemplate) + template, err := commontemplate.ResolveOrLookup(ctx, tm, cfg.Templates.ConversionsAPITag, NameConversionsAPITagTemplate, func() (*tagmanager2.CustomTemplate, error) { + return tm.LookupTemplate(ctx, NameConversionsAPITagTemplate) + }) if errors.Is(err, tagmanager.ErrNotFound) { l.Warn("Please install the 'Conversions API Tag' by 'facebookincubator' template manually first") return err diff --git a/pkg/provider/googleanalytics/server.go b/pkg/provider/googleanalytics/server.go index 90b8f66..69495cd 100644 --- a/pkg/provider/googleanalytics/server.go +++ b/pkg/provider/googleanalytics/server.go @@ -13,6 +13,7 @@ import ( "github.com/foomo/sesamy-cli/pkg/provider/googletag" "github.com/foomo/sesamy-cli/pkg/provider/googletagmanager" "github.com/foomo/sesamy-cli/pkg/tagmanager" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" 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" @@ -61,7 +62,12 @@ func Server(ctx context.Context, tm *tagmanager.TagManager, cfg config.GoogleAna } if cfg.EnableMPv2UserDataTransformation { - userDataTemplate, err := tm.UpsertCustomTemplate(ctx, servertemplate.NewJSONRequestValue(NameJSONRequestValueTemplate)) + jsonRequestValueTemplate, err := commontemplate.Resolve(cfg.Templates.JSONRequestValueVariable, NameJSONRequestValueTemplate, servertemplate.NewJSONRequestValue(NameJSONRequestValueTemplate)) + if err != nil { + return err + } + + userDataTemplate, err := tm.UpsertCustomTemplate(ctx, jsonRequestValueTemplate) if err != nil { return err } @@ -87,7 +93,12 @@ func Server(ctx context.Context, tm *tagmanager.TagManager, cfg config.GoogleAna } if cfg.GoogleGTagJSOverride.Enabled { - template, err := tm.UpsertCustomTemplate(ctx, googleanalyticstemplate.NewGoogleGTagClient(NameGoogleGTagClientTemplate)) + gtagClientTemplate, err := commontemplate.Resolve(cfg.Templates.GTagClient, NameGoogleGTagClientTemplate, googleanalyticstemplate.NewGoogleGTagClient(NameGoogleGTagClientTemplate)) + if err != nil { + return err + } + + template, err := tm.UpsertCustomTemplate(ctx, gtagClientTemplate) if err != nil { return err } diff --git a/pkg/provider/microsoftads/server.go b/pkg/provider/microsoftads/server.go index d08ed58..927f8a5 100644 --- a/pkg/provider/microsoftads/server.go +++ b/pkg/provider/microsoftads/server.go @@ -11,6 +11,7 @@ import ( "github.com/foomo/sesamy-cli/pkg/provider/microsoftads/server/template" "github.com/foomo/sesamy-cli/pkg/provider/microsoftads/server/trigger" "github.com/foomo/sesamy-cli/pkg/tagmanager" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" commonvariable "github.com/foomo/sesamy-cli/pkg/tagmanager/common/variable" "github.com/foomo/sesamy-cli/pkg/utils" "github.com/pkg/errors" @@ -28,7 +29,12 @@ func Server(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg } if cfg.Conversion.Enabled { - tagTemplate, err := tm.UpsertCustomTemplate(ctx, template.NewConversionTag(NameConversionsTagTemplate)) + conversionTemplate, err := commontemplate.Resolve(cfg.Templates.ConversionTag, NameConversionsTagTemplate, template.NewConversionTag(NameConversionsTagTemplate)) + if err != nil { + return err + } + + tagTemplate, err := tm.UpsertCustomTemplate(ctx, conversionTemplate) if err != nil { return err } diff --git a/pkg/provider/mixpanel/server.go b/pkg/provider/mixpanel/server.go index 55c4878..20d8809 100644 --- a/pkg/provider/mixpanel/server.go +++ b/pkg/provider/mixpanel/server.go @@ -13,6 +13,7 @@ import ( "github.com/foomo/sesamy-cli/pkg/provider/mixpanel/server/trigger" utmprovider "github.com/foomo/sesamy-cli/pkg/provider/utm" "github.com/foomo/sesamy-cli/pkg/tagmanager" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" commonvariable "github.com/foomo/sesamy-cli/pkg/tagmanager/common/variable" "github.com/foomo/sesamy-cli/pkg/tagmanager/server/variable" "github.com/foomo/sesamy-cli/pkg/utils" @@ -31,19 +32,14 @@ func Server(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg return err } - var template *tagmanager2.CustomTemplate - if cfg.LookupTagTemplate { - if template, err = tm.LookupTemplate(ctx, NameTagTemplate); err != nil { - if errors.Is(err, tagmanager.ErrNotFound) { - l.Warn("Please install the 'Mixpanel' by stape-io Tag Template manually first") - } + tagTemplate, err := commontemplate.Resolve(cfg.Templates.Tag, NameTagTemplate, mixpaneltemplate.NewMixpanelTag(NameTagTemplate)) + if err != nil { + return err + } - return err - } - } else { - if template, err = tm.UpsertCustomTemplate(ctx, mixpaneltemplate.NewMixpanelTag(NameTagTemplate)); err != nil { - return err - } + template, err := tm.UpsertCustomTemplate(ctx, tagTemplate) + if err != nil { + return err } projectToken, err := tm.UpsertVariable(ctx, folder, commonvariable.NewConstant(NamePrjectTokenConstant, cfg.ProjectToken)) diff --git a/pkg/provider/pinterest/server.go b/pkg/provider/pinterest/server.go index 4b49c3d..42941c7 100644 --- a/pkg/provider/pinterest/server.go +++ b/pkg/provider/pinterest/server.go @@ -10,8 +10,10 @@ import ( containertag "github.com/foomo/sesamy-cli/pkg/provider/pinterest/server/tag" "github.com/foomo/sesamy-cli/pkg/provider/pinterest/server/trigger" "github.com/foomo/sesamy-cli/pkg/tagmanager" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" "github.com/foomo/sesamy-cli/pkg/utils" "github.com/pkg/errors" + tagmanager2 "google.golang.org/api/tagmanager/v2" ) func Server(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg config.Pinterest) error { @@ -20,7 +22,9 @@ func Server(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg return err } - template, err := tm.LookupTemplate(ctx, NameTagTemplate) + template, err := commontemplate.ResolveOrLookup(ctx, tm, cfg.Templates.Tag, NameTagTemplate, func() (*tagmanager2.CustomTemplate, error) { + return tm.LookupTemplate(ctx, NameTagTemplate) + }) if err != nil { if errors.Is(err, tagmanager.ErrNotFound) { l.Warn("Please install the 'Pinterest API for Conversions Tag' Tag Template manually first") diff --git a/pkg/provider/tracify/server.go b/pkg/provider/tracify/server.go index 9af7b4d..91f5b7f 100644 --- a/pkg/provider/tracify/server.go +++ b/pkg/provider/tracify/server.go @@ -11,6 +11,7 @@ import ( "github.com/foomo/sesamy-cli/pkg/provider/tracify/server/template" "github.com/foomo/sesamy-cli/pkg/provider/tracify/server/trigger" "github.com/foomo/sesamy-cli/pkg/tagmanager" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" commonvariable "github.com/foomo/sesamy-cli/pkg/tagmanager/common/variable" "github.com/foomo/sesamy-cli/pkg/utils" "github.com/pkg/errors" @@ -33,7 +34,12 @@ func Server(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg return err } - tagTemplate, err := tm.UpsertCustomTemplate(ctx, template.NewTracifyTag(NameTracifyServerTagTemplate)) + tracifyTemplate, err := commontemplate.Resolve(cfg.Templates.Tag, NameTracifyServerTagTemplate, template.NewTracifyTag(NameTracifyServerTagTemplate)) + if err != nil { + return err + } + + tagTemplate, err := tm.UpsertCustomTemplate(ctx, tracifyTemplate) if err != nil { return err } diff --git a/pkg/provider/umami/server.go b/pkg/provider/umami/server.go index e11de31..d167109 100644 --- a/pkg/provider/umami/server.go +++ b/pkg/provider/umami/server.go @@ -10,6 +10,7 @@ import ( containertemplate "github.com/foomo/sesamy-cli/pkg/provider/umami/server/template" "github.com/foomo/sesamy-cli/pkg/provider/umami/server/trigger" "github.com/foomo/sesamy-cli/pkg/tagmanager" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" "github.com/foomo/sesamy-cli/pkg/utils" "github.com/pkg/errors" ) @@ -20,7 +21,12 @@ func Server(ctx context.Context, tm *tagmanager.TagManager, cfg config.Umami) er return err } - template, err := tm.UpsertCustomTemplate(ctx, containertemplate.NewUmami(Name)) + umamiTemplate, err := commontemplate.Resolve(cfg.Templates.Tag, Name, containertemplate.NewUmami(Name)) + if err != nil { + return err + } + + template, err := tm.UpsertCustomTemplate(ctx, umamiTemplate) if err != nil { return err } diff --git a/pkg/provider/utm/server.go b/pkg/provider/utm/server.go index cc7492a..eadf1bb 100644 --- a/pkg/provider/utm/server.go +++ b/pkg/provider/utm/server.go @@ -11,6 +11,7 @@ import ( "github.com/foomo/sesamy-cli/pkg/provider/utm/server/trigger" containervariable "github.com/foomo/sesamy-cli/pkg/provider/utm/server/variable" "github.com/foomo/sesamy-cli/pkg/tagmanager" + commontemplate "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" "github.com/pkg/errors" ) @@ -20,7 +21,12 @@ func Server(ctx context.Context, tm *tagmanager.TagManager, cfg config.Utm) erro return err } - variableTemplate, err := tm.UpsertCustomTemplate(ctx, containertemplate.NewUtmAttribution(NameUtmAttributionVariableTemplate)) + attributionTemplate, err := commontemplate.Resolve(cfg.Templates.AttributionVariable, NameUtmAttributionVariableTemplate, containertemplate.NewUtmAttribution(NameUtmAttributionVariableTemplate)) + if err != nil { + return err + } + + variableTemplate, err := tm.UpsertCustomTemplate(ctx, attributionTemplate) if err != nil { return err } @@ -29,7 +35,12 @@ func Server(ctx context.Context, tm *tagmanager.TagManager, cfg config.Utm) erro return err } - tagTemplate, err := tm.UpsertCustomTemplate(ctx, containertemplate.NewUtmAttributionCookieWriter(NameUtmAttributionCookieWriterTemplate)) + cookieWriterTemplate, err := commontemplate.Resolve(cfg.Templates.CookieWriterTag, NameUtmAttributionCookieWriterTemplate, containertemplate.NewUtmAttributionCookieWriter(NameUtmAttributionCookieWriterTemplate)) + if err != nil { + return err + } + + tagTemplate, err := tm.UpsertCustomTemplate(ctx, cookieWriterTemplate) if err != nil { return err } diff --git a/pkg/tagmanager/common/template/customtemplate.go b/pkg/tagmanager/common/template/customtemplate.go new file mode 100644 index 0000000..b90bee4 --- /dev/null +++ b/pkg/tagmanager/common/template/customtemplate.go @@ -0,0 +1,57 @@ +package template + +import ( + "context" + "os" + + "github.com/pkg/errors" + "google.golang.org/api/tagmanager/v2" +) + +// NewFromFile loads a custom template from the given path. The file contents are +// used verbatim, so it must be a complete GTM template. +func NewFromFile(name, filename string) (*tagmanager.CustomTemplate, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, errors.Wrap(err, "failed to read tag template: "+filename) + } + + return &tagmanager.CustomTemplate{ + Name: name, + TemplateData: string(data), + }, nil +} + +// Resolve returns the template loaded from filename, or embedded if filename is empty. +func Resolve(filename, name string, embedded *tagmanager.CustomTemplate) (*tagmanager.CustomTemplate, error) { + if filename == "" { + return embedded, nil + } + + return NewFromFile(name, filename) +} + +// Upserter provisions a custom template, e.g. tagmanager.TagManager. +type Upserter interface { + UpsertCustomTemplate(ctx context.Context, item *tagmanager.CustomTemplate) (*tagmanager.CustomTemplate, error) +} + +// ResolveOrLookup provisions the template from filename when set, otherwise it falls +// back to lookup, which is used by providers relying on a manually installed template. +func ResolveOrLookup( + ctx context.Context, + tm Upserter, + filename, name string, + lookup func() (*tagmanager.CustomTemplate, error), +) (*tagmanager.CustomTemplate, error) { + if filename == "" { + return lookup() + } + + item, err := NewFromFile(name, filename) + if err != nil { + return nil, err + } + + return tm.UpsertCustomTemplate(ctx, item) +} diff --git a/pkg/tagmanager/common/template/customtemplate_test.go b/pkg/tagmanager/common/template/customtemplate_test.go new file mode 100644 index 0000000..984b8a6 --- /dev/null +++ b/pkg/tagmanager/common/template/customtemplate_test.go @@ -0,0 +1,153 @@ +package template_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/foomo/sesamy-cli/pkg/tagmanager/common/template" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/api/tagmanager/v2" +) + +func TestNewFromFile(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + data string + filename string + wantErr bool + }{ + { + name: "reads template verbatim", + data: "___INFO___\n\n{\n \"type\": \"TAG\"\n}\n", + filename: "mixpanel.tpl", + }, + { + name: "keeps percent verbs untouched", + data: "___INFO___ 100% \"displayName\": \"%s\"\n", + filename: "verbs.tpl", + }, + { + name: "missing file", + filename: "does-not-exist.tpl", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), tt.filename) + if !tt.wantErr { + require.NoError(t, os.WriteFile(path, []byte(tt.data), 0o600)) + } + + actual, err := template.NewFromFile("Mixpanel", path) + if tt.wantErr { + require.Error(t, err) + assert.Nil(t, actual) + + return + } + + require.NoError(t, err) + assert.Equal(t, "Mixpanel", actual.Name) + assert.Equal(t, tt.data, actual.TemplateData) + }) + } +} + +func TestResolve(t *testing.T) { + t.Parallel() + + embedded := &tagmanager.CustomTemplate{Name: "Embedded", TemplateData: "___INFO___ embedded"} + + t.Run("empty path returns embedded", func(t *testing.T) { + t.Parallel() + + actual, err := template.Resolve("", "Custom", embedded) + require.NoError(t, err) + assert.Same(t, embedded, actual) + }) + + t.Run("path overrides embedded", func(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "custom.tpl") + require.NoError(t, os.WriteFile(path, []byte("___INFO___ custom"), 0o600)) + + actual, err := template.Resolve(path, "Custom", embedded) + require.NoError(t, err) + assert.Equal(t, "Custom", actual.Name) + assert.Equal(t, "___INFO___ custom", actual.TemplateData) + }) + + t.Run("missing path errors", func(t *testing.T) { + t.Parallel() + + actual, err := template.Resolve(filepath.Join(t.TempDir(), "nope.tpl"), "Custom", embedded) + require.Error(t, err) + assert.Nil(t, actual) + }) +} + +type upserterStub struct { + called bool + item *tagmanager.CustomTemplate +} + +func (u *upserterStub) UpsertCustomTemplate(_ context.Context, item *tagmanager.CustomTemplate) (*tagmanager.CustomTemplate, error) { + u.called = true + u.item = item + + return item, nil +} + +func TestResolveOrLookup(t *testing.T) { + t.Parallel() + + looked := &tagmanager.CustomTemplate{Name: "Gallery", TemplateData: "___INFO___ gallery"} + lookup := func() (*tagmanager.CustomTemplate, error) { return looked, nil } + + t.Run("empty path looks up without upserting", func(t *testing.T) { + t.Parallel() + + tm := &upserterStub{} + + actual, err := template.ResolveOrLookup(t.Context(), tm, "", "Custom", lookup) + require.NoError(t, err) + assert.Same(t, looked, actual) + assert.False(t, tm.called, "must not provision when no override is set") + }) + + t.Run("path upserts instead of looking up", func(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "custom.tpl") + require.NoError(t, os.WriteFile(path, []byte("___INFO___ custom"), 0o600)) + + tm := &upserterStub{} + + actual, err := template.ResolveOrLookup(t.Context(), tm, path, "Custom", lookup) + require.NoError(t, err) + require.True(t, tm.called, "must provision the custom template") + assert.Equal(t, "Custom", actual.Name) + assert.Equal(t, "___INFO___ custom", actual.TemplateData) + }) + + t.Run("missing path errors without upserting", func(t *testing.T) { + t.Parallel() + + tm := &upserterStub{} + + actual, err := template.ResolveOrLookup(t.Context(), tm, filepath.Join(t.TempDir(), "nope.tpl"), "Custom", lookup) + require.Error(t, err) + assert.Nil(t, actual) + assert.False(t, tm.called) + }) +} diff --git a/pkg/tagmanager/tagmanager.go b/pkg/tagmanager/tagmanager.go index 7f62ab4..5a34dfb 100644 --- a/pkg/tagmanager/tagmanager.go +++ b/pkg/tagmanager/tagmanager.go @@ -928,8 +928,8 @@ func (t *TagManager) UpsertCustomTemplate(ctx context.Context, item *tagmanager. } else if strings.Trim(item.TemplateData, "\n") == strings.Trim(cache.TemplateData, "\n") { l.Info("└ ✔︎ OK", "id", cache.TemplateId) } else { - edits := myers.ComputeEdits(span.URIFromPath("item.txt"), item.TemplateData, cache.TemplateData) - diff := gotextdiff.ToUnified("item.txt", "cache.txt", item.TemplateData, edits) + edits := myers.ComputeEdits(span.URIFromPath("item.tpl"), cache.TemplateData, item.TemplateData) + diff := gotextdiff.ToUnified("item.tpl", "cache.tpl", cache.TemplateData, edits) l.Info("└ 🔄 Update", "id", cache.TemplateId) l.Info(fmt.Sprint(diff)) diff --git a/sesamy.schema.json b/sesamy.schema.json index 741fda1..54abbc6 100644 --- a/sesamy.schema.json +++ b/sesamy.schema.json @@ -187,6 +187,10 @@ "regionSettings": { "$ref": "#/$defs/[]config.CookiebotRegionSetting", "description": "Default Consent state" + }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.CookiebotTemplates", + "description": "Custom tag template settings" } }, "additionalProperties": false, @@ -222,6 +226,16 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.CookiebotTemplates": { + "properties": { + "tag": { + "type": "string", + "description": "Path to a custom cmp tag template file, defaults to the manually installed one" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.Criteo": { "properties": { "enabled": { @@ -240,6 +254,10 @@ "type": "string", "description": "Criteo application id" }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.CriteoTemplates", + "description": "Custom tag template settings" + }, "googleConsent": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent", "description": "Google Consent settings" @@ -252,6 +270,20 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.CriteoTemplates": { + "properties": { + "eventsApiTag": { + "type": "string", + "description": "Path to a custom events api tag template file, defaults to the manually installed one" + }, + "userIdentification": { + "type": "string", + "description": "Path to a custom user identification template file, defaults to the manually installed one" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.Emarsys": { "properties": { "enabled": { @@ -270,6 +302,10 @@ "type": "boolean", "description": "Enable debug mode" }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.EmarsysTemplates", + "description": "Custom tag template settings" + }, "googleConsent": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent", "description": "Google Consent settings" @@ -286,6 +322,24 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.EmarsysTemplates": { + "properties": { + "webExtendTag": { + "type": "string", + "description": "Path to a custom web extend tag template file, defaults to the embedded one" + }, + "initializationClient": { + "type": "string", + "description": "Path to a custom initialization client template file, defaults to the embedded one" + }, + "initializationTag": { + "type": "string", + "description": "Path to a custom initialization tag template file, defaults to the embedded one" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.Facebook": { "properties": { "enabled": { @@ -301,6 +355,10 @@ "testEventToken": { "type": "string" }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.FacebookTemplates", + "description": "Custom tag template settings" + }, "googleConsent": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent", "description": "Google Consent settings" @@ -342,6 +400,16 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.FacebookTemplates": { + "properties": { + "conversionsApiTag": { + "type": "string", + "description": "Path to a custom conversions api tag template file, defaults to the manually installed one" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.GoogleAPI": { "properties": { "credentials": { @@ -466,6 +534,10 @@ "type": "boolean", "description": "Enable mpv2 user data transformation (experimental)" }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleAnalyticsTemplates", + "description": "Custom tag template settings" + }, "webContainer": { "$ref": "#/$defs/github.com.foomo.gocontemplate.pkg.contemplate.Config", "description": "Google Tag Manager web container settings" @@ -496,6 +568,20 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.GoogleAnalyticsTemplates": { + "properties": { + "jsonRequestValueVariable": { + "type": "string", + "description": "Path to a custom json request value variable template file, defaults to the embedded one" + }, + "gtagClient": { + "type": "string", + "description": "Path to a custom gtag client template file, defaults to the embedded one" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.GoogleConsent": { "properties": { "enabled": { @@ -659,6 +745,10 @@ "conversion": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.MicrosoftAdsConversion", "description": "Microsoft Ads Conversion settings" + }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.MicrosoftAdsTemplates", + "description": "Custom tag template settings" } }, "additionalProperties": false, @@ -705,6 +795,16 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.MicrosoftAdsTemplates": { + "properties": { + "conversionTag": { + "type": "string", + "description": "Path to a custom conversion tag template file, defaults to the embedded one" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.Mixpanel": { "properties": { "enabled": { @@ -715,10 +815,6 @@ "type": "string", "description": "Mixpanel project token" }, - "lookupTagTemplate": { - "type": "boolean", - "description": "Lookup tag template instead of provisioning it" - }, "googleConsent": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent", "description": "Google Consent settings" @@ -726,6 +822,10 @@ "serverContainer": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.MixpanelServerContainer", "description": "Google Tag Manager server container settings" + }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.MixpanelTemplates", + "description": "Custom tag template settings" } }, "additionalProperties": false, @@ -756,6 +856,16 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.MixpanelTemplates": { + "properties": { + "tag": { + "type": "string", + "description": "Path to a custom tag template file, defaults to the embedded one" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.Pinterest": { "properties": { "enabled": { @@ -774,6 +884,10 @@ "type": "boolean", "description": "Enable test mode" }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.PinterestTemplates", + "description": "Custom tag template settings" + }, "googleConsent": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent", "description": "Google Consent settings" @@ -786,6 +900,16 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.PinterestTemplates": { + "properties": { + "tag": { + "type": "string", + "description": "Path to a custom conversions tag template file, defaults to the manually installed one" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.Tracify": { "properties": { "enabled": { @@ -804,6 +928,10 @@ "type": "boolean", "description": "Enable stating mode" }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.TracifyTemplates", + "description": "Custom tag template settings" + }, "googleConsent": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent", "description": "Google Consent settings" @@ -816,6 +944,16 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.TracifyTemplates": { + "properties": { + "tag": { + "type": "string", + "description": "Path to a custom tag template file, defaults to the embedded one" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.TypeScript": { "properties": { "directory": { @@ -846,6 +984,10 @@ "endpointUrl": { "type": "string" }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.UmamiTemplates", + "description": "Custom tag template settings" + }, "googleConsent": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent", "description": "Google Consent settings" @@ -858,12 +1000,26 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.UmamiTemplates": { + "properties": { + "tag": { + "type": "string", + "description": "Path to a custom tag template file, defaults to the embedded one" + } + }, + "additionalProperties": false, + "type": "object" + }, "github.com.foomo.sesamy-cli.pkg.config.Utm": { "properties": { "enabled": { "type": "boolean", "description": "Enable provider" }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.UtmTemplates", + "description": "Custom tag template settings" + }, "googleConsent": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent", "description": "Google Consent settings" @@ -872,6 +1028,20 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.UtmTemplates": { + "properties": { + "attributionVariable": { + "type": "string", + "description": "Path to a custom attribution variable template file, defaults to the embedded one" + }, + "cookieWriterTag": { + "type": "string", + "description": "Path to a custom cookie writer tag template file, defaults to the embedded one" + } + }, + "additionalProperties": false, + "type": "object" + }, "map[string][]config.GoogleAdsConversionTracking": { "additionalProperties": { "$ref": "#/$defs/[]config.GoogleAdsConversionTracking"