diff --git a/bindings/twilio/sendgrid/metadata.yaml b/bindings/twilio/sendgrid/metadata.yaml index f645420b1b..463b14076d 100644 --- a/bindings/twilio/sendgrid/metadata.yaml +++ b/bindings/twilio/sendgrid/metadata.yaml @@ -61,3 +61,7 @@ metadata: required: false description: "The dynamic template ID" example: "your-template-id" + - name: trackingSettings + required: false + description: "JSON object controlling SendGrid email tracking, using SendGrid API field names (click_tracking, open_tracking, subscription_tracking, ganalytics)" + example: '{"click_tracking":{"enable":true,"enable_text":true},"open_tracking":{"enable":true},"subscription_tracking":{"enable":false},"ganalytics":{"enable":true,"utm_source":"transactional-email","utm_medium":"email"}}' diff --git a/bindings/twilio/sendgrid/sendgrid.go b/bindings/twilio/sendgrid/sendgrid.go index 2e4fc230a3..49b2d61ed5 100644 --- a/bindings/twilio/sendgrid/sendgrid.go +++ b/bindings/twilio/sendgrid/sendgrid.go @@ -49,8 +49,10 @@ type sendGridMetadata struct { EmailBcc string `mapstructure:"emailBcc"` DynamicTemplateData string `mapstructure:"dynamicTemplateData"` DynamicTemplateID string `mapstructure:"dynamicTemplateId"` + TrackingSettings string `mapstructure:"trackingSettings"` - dynamicTemplateDataCache map[string]any // Cache the unmarshalled dynamic template data + dynamicTemplateDataCache map[string]any // Cache the unmarshalled dynamic template data + trackingSettingsCache *mail.TrackingSettings // Cache the unmarshalled tracking settings } // Wrapper to help decode SendGrid API errors. @@ -89,6 +91,15 @@ func (sg *SendGrid) parseMetadata(meta bindings.Metadata) (sendGridMetadata, err } } + // Cache the unmarshalled tracking settings if present + if sgMeta.TrackingSettings != "" { + trackingSettings, trackingError := UnmarshalTrackingSettings(sgMeta.TrackingSettings) + if trackingError != nil { + return sgMeta, trackingError + } + sgMeta.trackingSettingsCache = trackingSettings + } + return sgMeta, nil } @@ -212,6 +223,18 @@ func (sg *SendGrid) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*b templateData = sg.metadata.dynamicTemplateDataCache } + // Build email tracking settings, this is optional + var trackingSettings *mail.TrackingSettings + if req.Metadata["trackingSettings"] != "" { + var trackingError error + trackingSettings, trackingError = UnmarshalTrackingSettings(req.Metadata["trackingSettings"]) + if trackingError != nil { + return nil, trackingError + } + } else if sg.metadata.trackingSettingsCache != nil { + trackingSettings = sg.metadata.trackingSettingsCache + } + // Email body is held in req.Data, after we tidy it up a bit emailBody, err := strconv.Unquote(string(req.Data)) if err != nil { @@ -240,6 +263,9 @@ func (sg *SendGrid) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*b if templateData != nil { personalization.DynamicTemplateData = templateData } + if trackingSettings != nil { + email.SetTrackingSettings(trackingSettings) + } email.AddPersonalizations(personalization) @@ -284,6 +310,17 @@ func UnmarshalDynamicTemplateData(jsonString string, result *map[string]any) err return nil } +// Function that unmarshals a tracking settings JSON string, using the SendGrid API field names +// (click_tracking, open_tracking, subscription_tracking, ganalytics). +func UnmarshalTrackingSettings(jsonString string) (*mail.TrackingSettings, error) { + trackingSettings := &mail.TrackingSettings{} + err := json.Unmarshal([]byte(jsonString), trackingSettings) + if err != nil { + return nil, fmt.Errorf("error from SendGrid binding, tracking settings are not valid JSON: %w", err) + } + return trackingSettings, nil +} + func Close() error { return nil } diff --git a/bindings/twilio/sendgrid/sendgrid_test.go b/bindings/twilio/sendgrid/sendgrid_test.go index 1fc6ab064c..22ccd7f2ab 100644 --- a/bindings/twilio/sendgrid/sendgrid_test.go +++ b/bindings/twilio/sendgrid/sendgrid_test.go @@ -93,6 +93,86 @@ func TestParseMetadataWithOptionalNames(t *testing.T) { }) } +func TestParseMetadataWithTrackingSettings(t *testing.T) { + logger := logger.NewLogger("test") + + trackingSettingsJSON := `{ + "click_tracking": {"enable": true, "enable_text": true}, + "open_tracking": {"enable": true, "substitution_tag": "%open_track_tag%"}, + "subscription_tracking": {"enable": false}, + "ganalytics": {"enable": true, "utm_source": "transactional-email", "utm_medium": "email", "utm_campaign": "order-delivery"} + }` + + t.Run("Has correct tracking settings metadata", func(t *testing.T) { + m := bindings.Metadata{} + m.Properties = map[string]string{ + "apiKey": "123", + "emailFrom": "test1@example.net", + "emailTo": "test2@example.net", + "subject": "hello", + "trackingSettings": trackingSettingsJSON, + } + r := SendGrid{logger: logger} + sgMeta, err := r.parseMetadata(m) + require.NoError(t, err) + require.NotNil(t, sgMeta.trackingSettingsCache) + assert.True(t, *sgMeta.trackingSettingsCache.ClickTracking.Enable) + assert.True(t, *sgMeta.trackingSettingsCache.ClickTracking.EnableText) + assert.True(t, *sgMeta.trackingSettingsCache.OpenTracking.Enable) + assert.Equal(t, "%open_track_tag%", sgMeta.trackingSettingsCache.OpenTracking.SubstitutionTag) + assert.False(t, *sgMeta.trackingSettingsCache.SubscriptionTracking.Enable) + assert.True(t, *sgMeta.trackingSettingsCache.GoogleAnalytics.Enable) + assert.Equal(t, "transactional-email", sgMeta.trackingSettingsCache.GoogleAnalytics.CampaignSource) + assert.Equal(t, "email", sgMeta.trackingSettingsCache.GoogleAnalytics.CampaignMedium) + assert.Equal(t, "order-delivery", sgMeta.trackingSettingsCache.GoogleAnalytics.CampaignName) + }) + + t.Run("Has incorrect tracking settings metadata", func(t *testing.T) { + m := bindings.Metadata{} + m.Properties = map[string]string{ + "apiKey": "123", + "emailFrom": "test1@example.net", + "emailTo": "test2@example.net", + "subject": "hello", + "trackingSettings": `{"wrong"}`, + } + r := SendGrid{logger: logger} + _, err := r.parseMetadata(m) + require.Error(t, err) + }) + + t.Run("No tracking settings metadata leaves cache nil", func(t *testing.T) { + m := bindings.Metadata{} + m.Properties = map[string]string{ + "apiKey": "123", + "emailFrom": "test1@example.net", + "emailTo": "test2@example.net", + "subject": "hello", + } + r := SendGrid{logger: logger} + sgMeta, err := r.parseMetadata(m) + require.NoError(t, err) + assert.Nil(t, sgMeta.trackingSettingsCache) + }) +} + +// Test UnmarshalTrackingSettings function +func TestUnmarshalTrackingSettings(t *testing.T) { + t.Run("Valid tracking settings JSON", func(t *testing.T) { + trackingSettings, err := UnmarshalTrackingSettings(`{"click_tracking":{"enable":true},"open_tracking":{"enable":false}}`) + require.NoError(t, err) + assert.True(t, *trackingSettings.ClickTracking.Enable) + assert.False(t, *trackingSettings.OpenTracking.Enable) + assert.Nil(t, trackingSettings.SubscriptionTracking) + assert.Nil(t, trackingSettings.GoogleAnalytics) + }) + + t.Run("Invalid tracking settings JSON", func(t *testing.T) { + _, err := UnmarshalTrackingSettings(`{"wrong"}`) + require.Error(t, err) + }) +} + // Test UnmarshalDynamicTemplateData function func TestUnmarshalDynamicTemplateData(t *testing.T) { t.Run("Test Template Data", func(t *testing.T) {