From e2bd46f4956804058ba7a60bf2bd0baaa39cf927 Mon Sep 17 00:00:00 2001 From: Kevin Franklin Kim Date: Wed, 16 Sep 2026 16:11:40 +0200 Subject: [PATCH] feat: add OpenAI Ads provider Introduce support for the OpenAI Ads provider with full server-side conversion tracking via the Conversions API. Includes configuration options, custom template integration, and documentation updates. --- docs/.vitepress/config.mts | 1 + docs/providers/index.md | 1 + docs/providers/openai-ads.md | 49 +++++++ docs/reference/configuration.md | 1 + internal/cli/provision/server.go | 9 ++ internal/cli/tags.go | 2 + pkg/config/config.go | 2 + pkg/config/openaiads.go | 21 +++ pkg/config/openaiadsconversion.go | 32 +++++ pkg/provider/openaiads/constants.go | 9 ++ pkg/provider/openaiads/server.go | 83 ++++++++++++ .../openaiads/server/tag/conversion.go | 125 ++++++++++++++++++ .../server/trigger/conversionevent.go | 76 +++++++++++ sesamy.schema.json | 93 +++++++++++++ 14 files changed, 504 insertions(+) create mode 100644 docs/providers/openai-ads.md create mode 100644 pkg/config/openaiads.go create mode 100644 pkg/config/openaiadsconversion.go create mode 100644 pkg/provider/openaiads/constants.go create mode 100644 pkg/provider/openaiads/server.go create mode 100644 pkg/provider/openaiads/server/tag/conversion.go create mode 100644 pkg/provider/openaiads/server/trigger/conversionevent.go diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 335e7ab..f99eb44 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -61,6 +61,7 @@ export default defineConfig({ {text: 'Facebook', link: '/providers/facebook'}, {text: 'Pinterest', link: '/providers/pinterest'}, {text: 'Microsoft Ads', link: '/providers/microsoft-ads'}, + {text: 'OpenAI Ads', link: '/providers/openai-ads'}, {text: 'Criteo', link: '/providers/criteo'}, {text: 'Emarsys', link: '/providers/emarsys'}, {text: 'Mixpanel', link: '/providers/mixpanel'}, diff --git a/docs/providers/index.md b/docs/providers/index.md index eda4793..833b2d3 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -13,6 +13,7 @@ Each provider is a self-contained module under `pkg/provider/` that knows how to | [Facebook](./facebook) | — | ✔ | ad | AddToCart, Purchase, ... | | [Pinterest](./pinterest) | — | ✔ | analytics | AddToCart, Purchase, ... | | [Microsoft Ads](./microsoft-ads) | — | ✔ | ad | conversions | +| [OpenAI Ads](./openai-ads) | — | ✔ | ad | conversions | | [Criteo](./criteo) | — | ✔ | — | AddToCart, Purchase, ... | | [Emarsys](./emarsys) | — | ✔ | analytics | Purchase, ViewItem, ... | | [Mixpanel](./mixpanel) | — | ✔ | analytics | PageView, Purchase, ... | diff --git a/docs/providers/openai-ads.md b/docs/providers/openai-ads.md new file mode 100644 index 0000000..423e034 --- /dev/null +++ b/docs/providers/openai-ads.md @@ -0,0 +1,49 @@ +# OpenAI Ads + +OpenAI Ads server-side conversion tracking via the Conversions API. + +::: warning +This provider relies on the **OpenAI Ads Conversions API by Stape** tag template. Install it into your server +container from the GTM Community Template Gallery before provisioning, or point `templates.conversionsApiTag` at a +local copy. +::: + +## Configuration + +```yaml +openAiAds: + enabled: true + pixelId: '' + apiKey: '' + templates: + conversionsApiTag: '' + googleConsent: + enabled: true + mode: ad_storage + conversion: + enabled: true + validateOnly: false + serverContainer: + directory: . + settings: + page_view: + eventName: page_viewed + packages: + - path: github.com/foomo/sesamy-go/pkg/event + types: + - Purchase +``` + +| Field | Purpose | +|-------|---------| +| `pixelId` | OpenAI Ads pixel ID. | +| `apiKey` | OpenAI Ads API key. | +| `googleConsent.mode` | Defaults to `ad_storage`. | +| `conversion.validateOnly` | Validate events without persisting them. Applies to every event. | +| `conversion.serverContainer.settings..eventName` | Standard OpenAI Ads event name. Defaults to the event name. | +| `conversion.serverContainer.packages` | Events to wire conversion tags for. | +| `templates.conversionsApiTag` | Path to a custom conversions api tag template file. Empty (default) uses the manually installed one. | + +::: 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/reference/configuration.md b/docs/reference/configuration.md index 376bf8f..2d0d362 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -18,6 +18,7 @@ The full top-level schema of `sesamy.yaml` (version `1.1`). For provider-specifi | `facebook` | object | [Facebook provider config](/providers/facebook). | | `pinterest` | object | [Pinterest provider config](/providers/pinterest). | | `microsoftAds` | object | [Microsoft Ads provider config](/providers/microsoft-ads). | +| `openAiAds` | object | [OpenAI Ads provider config](/providers/openai-ads). | | `criteo` | object | [Criteo provider config](/providers/criteo). | | `emarsys` | object | [Emarsys provider config](/providers/emarsys). | | `mixpanel` | object | [Mixpanel provider config](/providers/mixpanel). | diff --git a/internal/cli/provision/server.go b/internal/cli/provision/server.go index 123c21d..92c48f8 100644 --- a/internal/cli/provision/server.go +++ b/internal/cli/provision/server.go @@ -14,6 +14,7 @@ import ( googletagmanagerprovider "github.com/foomo/sesamy-cli/pkg/provider/googletagmanager" microsoftadsprovider "github.com/foomo/sesamy-cli/pkg/provider/microsoftads" mixpanelprovider "github.com/foomo/sesamy-cli/pkg/provider/mixpanel" + openaiadsprovider "github.com/foomo/sesamy-cli/pkg/provider/openaiads" pinterestprovider "github.com/foomo/sesamy-cli/pkg/provider/pinterest" tracifyprovider "github.com/foomo/sesamy-cli/pkg/provider/tracify" umamiprovider "github.com/foomo/sesamy-cli/pkg/provider/umami" @@ -156,6 +157,14 @@ func NewServer(l *slog.Logger) *cobra.Command { } } + if cfg.OpenAIAds.Enabled && utils.Tag(openaiadsprovider.Tag, tags) { + l.Info("🅿️ Running provider", "name", openaiadsprovider.Name, "tag", openaiadsprovider.Tag) + + if err := openaiadsprovider.Server(cmd.Context(), l, tm, cfg.OpenAIAds); err != nil { + return errors.Wrap(err, "failed to provision openaiads") + } + } + if cfg.Mixpanel.Enabled && utils.Tag(mixpanelprovider.Tag, tags) { l.Info("🅿️ Running provider", "name", mixpanelprovider.Name, "tag", mixpanelprovider.Tag) diff --git a/internal/cli/tags.go b/internal/cli/tags.go index d6c2d0c..73d2b57 100644 --- a/internal/cli/tags.go +++ b/internal/cli/tags.go @@ -18,6 +18,7 @@ import ( "github.com/foomo/sesamy-cli/pkg/provider/hotjar" "github.com/foomo/sesamy-cli/pkg/provider/microsoftads" "github.com/foomo/sesamy-cli/pkg/provider/mixpanel" + "github.com/foomo/sesamy-cli/pkg/provider/openaiads" "github.com/foomo/sesamy-cli/pkg/provider/pinterest" "github.com/foomo/sesamy-cli/pkg/provider/tracify" "github.com/foomo/sesamy-cli/pkg/provider/umami" @@ -46,6 +47,7 @@ func NewTags(l *slog.Logger) *cobra.Command { hotjar.Name: hotjar.Tag, microsoftads.Name: microsoftads.Tag, mixpanel.Name: mixpanel.Tag, + openaiads.Name: openaiads.Tag, tracify.Name: tracify.Tag, umami.Name: umami.Tag, pinterest.Name: pinterest.Tag, diff --git a/pkg/config/config.go b/pkg/config/config.go index bdc8673..600a486 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -26,6 +26,8 @@ type Config struct { Facebook Facebook `json:"facebook" yaml:"facebook"` // MicrosoftAds provider settings MicrosoftAds MicrosoftAds `json:"microsoftAds" yaml:"microsoftAds"` + // OpenAIAds provider settings + OpenAIAds OpenAIAds `json:"openaiAds" yaml:"openaiAds"` // Mixpanel provider settings Mixpanel Mixpanel `json:"mixpanel" yaml:"mixpanel"` // Pinterest provider settings diff --git a/pkg/config/openaiads.go b/pkg/config/openaiads.go new file mode 100644 index 0000000..3f8a4c0 --- /dev/null +++ b/pkg/config/openaiads.go @@ -0,0 +1,21 @@ +package config + +type OpenAIAds struct { + // Enable provider + Enabled bool `json:"enabled" yaml:"enabled"` + // Google Consent settings + GoogleConsent GoogleConsent `json:"googleConsent" yaml:"googleConsent"` + // OpenAI Ads pixel id + PixelID string `json:"pixelId" yaml:"pixelId"` + // OpenAI Ads api key + APIKey string `json:"apiKey" yaml:"apiKey"` + // OpenAI Ads Conversion settings + Conversion OpenAIAdsConversion `json:"conversion" yaml:"conversion"` + // Custom tag template settings + Templates OpenAIAdsTemplates `json:"templates" yaml:"templates"` +} + +type OpenAIAdsTemplates struct { + // Path to a custom conversions api tag template file, defaults to the manually installed one + ConversionsAPITag string `json:"conversionsApiTag" yaml:"conversionsApiTag"` +} diff --git a/pkg/config/openaiadsconversion.go b/pkg/config/openaiadsconversion.go new file mode 100644 index 0000000..0e8e1b1 --- /dev/null +++ b/pkg/config/openaiadsconversion.go @@ -0,0 +1,32 @@ +package config + +import ( + "github.com/foomo/gocontemplate/pkg/contemplate" +) + +type OpenAIAdsConversion struct { + // Enable provider + Enabled bool `json:"enabled" yaml:"enabled"` + // Only validate the events without persisting them + ValidateOnly bool `json:"validateOnly" yaml:"validateOnly"` + // Google Tag Manager server container settings + ServerContainer OpenAIAdsServerContainer `json:"serverContainer" yaml:"serverContainer"` +} + +type OpenAIAdsServerContainer struct { + contemplate.Config `json:",inline" yaml:",squash"` + Settings map[string]OpenAIAdsConversionTag `json:"settings" yaml:"settings"` +} + +type OpenAIAdsConversionTag struct { + // Standard event name sent to OpenAI Ads, defaults to the event name + EventName string `json:"eventName" yaml:"eventName"` +} + +func (s *OpenAIAdsServerContainer) Setting(eventName string) OpenAIAdsConversionTag { + if value, ok := s.Settings[eventName]; ok { + return value + } + + return OpenAIAdsConversionTag{} +} diff --git a/pkg/provider/openaiads/constants.go b/pkg/provider/openaiads/constants.go new file mode 100644 index 0000000..e883164 --- /dev/null +++ b/pkg/provider/openaiads/constants.go @@ -0,0 +1,9 @@ +package openaiads + +const ( + Tag = "oaiads" + Name = "OpenAI Ads" + NamePixelIDConstant = "OpenAI Ads Pixel ID" + NameAPIKeyConstant = "OpenAI Ads API Key" + NameConversionsAPITagTemplate = "OpenAI Ads Conversions API by Stape" +) diff --git a/pkg/provider/openaiads/server.go b/pkg/provider/openaiads/server.go new file mode 100644 index 0000000..5934464 --- /dev/null +++ b/pkg/provider/openaiads/server.go @@ -0,0 +1,83 @@ +package openaiads + +import ( + "context" + "log/slog" + + "github.com/foomo/sesamy-cli/pkg/config" + "github.com/foomo/sesamy-cli/pkg/provider/googleconsent" + googleconsentvariable "github.com/foomo/sesamy-cli/pkg/provider/googleconsent/server/variable" + servertagx "github.com/foomo/sesamy-cli/pkg/provider/openaiads/server/tag" + "github.com/foomo/sesamy-cli/pkg/provider/openaiads/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" + tagmanager2 "google.golang.org/api/tagmanager/v2" +) + +func Server(ctx context.Context, l *slog.Logger, tm *tagmanager.TagManager, cfg config.OpenAIAds) error { + folder, err := tm.UpsertFolder(ctx, "Sesamy - "+Name) + if err != nil { + return err + } + + pixelID, err := tm.UpsertVariable(ctx, folder, commonvariable.NewConstant(NamePixelIDConstant, cfg.PixelID)) + if err != nil { + return err + } + + apiKey, err := tm.UpsertVariable(ctx, folder, commonvariable.NewConstant(NameAPIKeyConstant, cfg.APIKey)) + if err != nil { + return err + } + + if cfg.Conversion.Enabled { + tagTemplate, err := commontemplate.ResolveOrLookup(ctx, tm, cfg.Templates.ConversionsAPITag, NameConversionsAPITagTemplate, func() (*tagmanager2.CustomTemplate, error) { + return tm.LookupTemplate(ctx, NameConversionsAPITagTemplate) + }) + if err != nil { + if errors.Is(err, tagmanager.ErrNotFound) { + l.Warn("Please install the '" + NameConversionsAPITagTemplate + "' Tag Template manually first") + } + + return err + } + + { // create tags + eventParameters, err := utils.LoadEventParams(ctx, cfg.Conversion.ServerContainer.Config) + if err != nil { + return err + } + + for event := range eventParameters { + var eventTriggerOpts []trigger.ConversionEventOption + + if cfg.GoogleConsent.Enabled { + if err := googleconsent.ServerEnsure(ctx, tm); err != nil { + return err + } + + consentVariable, err := tm.LookupVariable(ctx, googleconsentvariable.GoogleConsentModeName(cfg.GoogleConsent.Mode)) + if err != nil { + return err + } + + eventTriggerOpts = append(eventTriggerOpts, trigger.ConversionEventWithConsentMode(consentVariable)) + } + + eventTrigger, err := tm.UpsertTrigger(ctx, folder, trigger.NewConversionEvent(event, eventTriggerOpts...)) + if err != nil { + return errors.Wrap(err, "failed to upsert event trigger: "+event) + } + + if _, err := tm.UpsertTag(ctx, folder, servertagx.NewConversion(event, pixelID, apiKey, tagTemplate, cfg.Conversion.ValidateOnly, cfg.Conversion.ServerContainer.Setting(event), eventTrigger)); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/pkg/provider/openaiads/server/tag/conversion.go b/pkg/provider/openaiads/server/tag/conversion.go new file mode 100644 index 0000000..e2b82eb --- /dev/null +++ b/pkg/provider/openaiads/server/tag/conversion.go @@ -0,0 +1,125 @@ +package tag + +import ( + "strconv" + + "github.com/foomo/sesamy-cli/pkg/config" + "github.com/foomo/sesamy-cli/pkg/utils" + "google.golang.org/api/tagmanager/v2" +) + +func ConversionName(v string) string { + return "OpenAI Ads Conversion - " + v +} + +func NewConversion(name string, pixelID, apiKey *tagmanager.Variable, template *tagmanager.CustomTemplate, validateOnly bool, settings config.OpenAIAdsConversionTag, triggers ...*tagmanager.Trigger) *tagmanager.Tag { + eventName := settings.EventName + eventNameSetup := "standard" + if eventName == "" { + eventName = name + eventNameSetup = "inherit" + } + + return &tagmanager.Tag{ + FiringTriggerId: utils.TriggerIDs(triggers), + Name: ConversionName(name), + TagFiringOption: "oncePerEvent", + Parameter: []*tagmanager.Parameter{ + { + Key: "pixelId", + Type: "template", + Value: "{{" + pixelID.Name + "}}", + }, + { + Key: "apiKey", + Type: "template", + Value: "{{" + apiKey.Name + "}}", + }, + { + Key: "eventNameSetup", + Type: "template", + Value: eventNameSetup, + }, + { + Key: "eventNameStandard", + Type: "template", + Value: eventName, + }, + { + Key: "actionSource", + Type: "template", + Value: "web", + }, + { + Key: "adStorageConsent", + Type: "template", + Value: "optional", + }, + { + Key: "validateOnly", + Type: "boolean", + Value: strconv.FormatBool(validateOnly), + }, + { + Key: "autoMapEventParameters", + Type: "boolean", + Value: "true", + }, + { + Key: "autoMapUserDataParameters", + Type: "boolean", + Value: "true", + }, + { + Key: "autoMapServerEventDataParameters", + Type: "boolean", + Value: "true", + }, + { + Key: "useOptimisticScenario", + Type: "boolean", + Value: "false", + }, + { + Key: "itemIdKey", + Type: "template", + }, + { + Key: "setBrowserIdCookie", + Type: "boolean", + Value: "true", + }, + { + Key: "setClickIdCookie", + Type: "boolean", + Value: "true", + }, + { + Key: "cookieDomain", + Type: "template", + Value: "auto", + }, + { + Key: "cookieExpiration", + Type: "template", + Value: "30", + }, + { + Key: "cookieExpirationBrowserId", + Type: "template", + Value: "365", + }, + { + Key: "cookieHttpOnly", + Type: "boolean", + Value: "true", + }, + { + Key: "cookieSameSite", + Type: "template", + Value: "lax", + }, + }, + Type: utils.TemplateType(template), + } +} diff --git a/pkg/provider/openaiads/server/trigger/conversionevent.go b/pkg/provider/openaiads/server/trigger/conversionevent.go new file mode 100644 index 0000000..d061bdc --- /dev/null +++ b/pkg/provider/openaiads/server/trigger/conversionevent.go @@ -0,0 +1,76 @@ +package trigger + +import ( + "google.golang.org/api/tagmanager/v2" +) + +func ConversionEventName(v string) string { + return "OpenAI Ads Conversion - " + v +} + +type ( + ConversionEventOptions struct { + consentMode *tagmanager.Variable + } + ConversionEventOption func(*ConversionEventOptions) +) + +func ConversionEventWithConsentMode(mode *tagmanager.Variable) ConversionEventOption { + return func(o *ConversionEventOptions) { + o.consentMode = mode + } +} + +func NewConversionEvent(name string, opts ...ConversionEventOption) *tagmanager.Trigger { + o := &ConversionEventOptions{} + + for _, opt := range opts { + if opt != nil { + opt(o) + } + } + + var filter []*tagmanager.Condition + if o.consentMode != nil { + filter = append(filter, + &tagmanager.Condition{ + Parameter: []*tagmanager.Parameter{ + { + Key: "arg0", + Type: "template", + Value: "{{" + o.consentMode.Name + "}}", + }, + { + Key: "arg1", + Type: "template", + Value: "granted", + }, + }, + Type: "equals", + }, + ) + } + + return &tagmanager.Trigger{ + Type: "customEvent", + Name: ConversionEventName(name), + CustomEventFilter: []*tagmanager.Condition{ + { + Parameter: []*tagmanager.Parameter{ + { + Key: "arg0", + Type: "template", + Value: "{{_event}}", + }, + { + Key: "arg1", + Type: "template", + Value: name, + }, + }, + Type: "equals", + }, + }, + Filter: filter, + } +} diff --git a/sesamy.schema.json b/sesamy.schema.json index 54abbc6..4ad71d7 100644 --- a/sesamy.schema.json +++ b/sesamy.schema.json @@ -101,6 +101,10 @@ "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.MicrosoftAds", "description": "MicrosoftAds provider settings" }, + "openaiAds": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.OpenAIAds", + "description": "OpenAIAds provider settings" + }, "mixpanel": { "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.Mixpanel", "description": "Mixpanel provider settings" @@ -866,6 +870,89 @@ "additionalProperties": false, "type": "object" }, + "github.com.foomo.sesamy-cli.pkg.config.OpenAIAds": { + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable provider" + }, + "googleConsent": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.GoogleConsent", + "description": "Google Consent settings" + }, + "pixelId": { + "type": "string", + "description": "OpenAI Ads pixel id" + }, + "apiKey": { + "type": "string", + "description": "OpenAI Ads api key" + }, + "conversion": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.OpenAIAdsConversion", + "description": "OpenAI Ads Conversion settings" + }, + "templates": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.OpenAIAdsTemplates", + "description": "Custom tag template settings" + } + }, + "additionalProperties": false, + "type": "object" + }, + "github.com.foomo.sesamy-cli.pkg.config.OpenAIAdsConversion": { + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable provider" + }, + "validateOnly": { + "type": "boolean", + "description": "Only validate the events without persisting them" + }, + "serverContainer": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.OpenAIAdsServerContainer", + "description": "Google Tag Manager server container settings" + } + }, + "additionalProperties": false, + "type": "object" + }, + "github.com.foomo.sesamy-cli.pkg.config.OpenAIAdsConversionTag": { + "properties": { + "eventName": { + "type": "string", + "description": "Standard event name sent to OpenAI Ads, defaults to the event name" + } + }, + "additionalProperties": false, + "type": "object" + }, + "github.com.foomo.sesamy-cli.pkg.config.OpenAIAdsServerContainer": { + "properties": { + "directory": { + "type": "string" + }, + "packages": { + "$ref": "#/$defs/[]*contemplate.PackageConfig" + }, + "settings": { + "$ref": "#/$defs/map[string]config.OpenAIAdsConversionTag" + } + }, + "additionalProperties": false, + "type": "object" + }, + "github.com.foomo.sesamy-cli.pkg.config.OpenAIAdsTemplates": { + "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.Pinterest": { "properties": { "enabled": { @@ -1066,6 +1153,12 @@ }, "type": "object" }, + "map[string]config.OpenAIAdsConversionTag": { + "additionalProperties": { + "$ref": "#/$defs/github.com.foomo.sesamy-cli.pkg.config.OpenAIAdsConversionTag" + }, + "type": "object" + }, "map[string]string": { "additionalProperties": { "type": "string"