Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bindings/twilio/sendgrid/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}'
39 changes: 38 additions & 1 deletion bindings/twilio/sendgrid/sendgrid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
}
80 changes: 80 additions & 0 deletions bindings/twilio/sendgrid/sendgrid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down