Skip to content

feat(coderd): add company logo when available for email notifications #14935

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Oct 22, 2024

Conversation

defelmnq
Copy link
Contributor

@defelmnq defelmnq commented Oct 2, 2024

This PR aims to close #14253

We keep the default behavior using the Coder logo if there's no logo set.
Otherwise we want to use the logo based on the URL set in appearance.

Copy link

github-actions bot commented Oct 2, 2024

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@stirby
Copy link
Collaborator

stirby commented Oct 2, 2024

@defelmnq Assuming this closes #14253.

I'm concerned about how the aspect ratio/inclusion of text impacts display. Right now, I believe the logo we use in the email includes "Coder" as text. The logo we use on the dashboard does not.

We may want to include whatever name they replace "Coder" with in the deployment appearance settings in the email as well.

@defelmnq defelmnq changed the title feat(notifications): add company logo when available for email notifications feat(coderd): add company logo when available for email notifications Oct 3, 2024
@defelmnq
Copy link
Contributor Author

defelmnq commented Oct 3, 2024

I have read the CLA Document and I hereby sign the CLA

cdrcommunity added a commit to coder/cla that referenced this pull request Oct 3, 2024
@defelmnq
Copy link
Contributor Author

defelmnq commented Oct 3, 2024

@stirby Indeed the objective is to resolve #14253

You're totally right - I just have a question as we recommend using a 3:1 aspect ratio image (We recommend a transparent image with 3:1 aspect ratio.)

Should we put the name next to the logo anyway or maybe below ?

I will update the PR to add the value in the meantime.

@defelmnq defelmnq marked this pull request as ready for review October 3, 2024 09:54
@mtojek mtojek requested review from dannykopping and mtojek October 3, 2024 10:10
logoURL = NotificationsDefaultLogoURL
}

payload.Labels["_logo_url"] = logoURL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking loud:

Logo URL does not seem to be a Label or Data. Maybe we need to introduce another property like Meta? Or just put it on the same level as title and body.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @mtojek here.

I don't think this should go into the payload. It's going to be the same for every notification, and if it's in the payload then we'll be storing the same URL for every notification.

Have a look at coderd/notifications/dispatch/smtp/html.gotmpl: you'll see {{ base_url }}. This is provided by a helper function, defined in cli/server.go`:

// templateHelpers builds a set of functions which can be called in templates.
// We build them here to avoid an import cycle by using coderd.Options in notifications.Manager.
// We can later use this to inject whitelabel fields when app name / logo URL are overridden. <----
func templateHelpers(options *coderd.Options) map[string]any {
	return map[string]any{
		"base_url":     func() string { return options.AccessURL.String() },
		"current_year": func() string { return strconv.Itoa(time.Now().Year()) },
	}
}

We should create a new helper which retrieves the logo instead, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, indeed it makes complete sense 🙏

I modified the approach to have both logo_url and app_name defined in the templateHelpers managinf both the normal flow, default value, and error cases. Keen to have your opinion on it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super 👍

@@ -223,6 +229,18 @@ func (n *notifier) prepare(ctx context.Context, msg database.AcquireNotification
return nil, xerrors.Errorf("failed to resolve handler %q", msg.Method)
}

logoURL, err := n.store.GetLogoURL(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
n.log.Error(ctx, "failed fetching logo url", slog.Error(err))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting use case. Some customers prefer to use their company logos instead of Coder default one. I think we should rather fail fast the notifier flow than default to Coder logo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @mtojek @mafredri as you both raised this one. In case of errors I can :

  • add default values (seems we do not want to avoid displaying Coder information to customers that prefer not.)
  • skip the logo or name
  • retry the notification

As this situation means that we had a failure fetching data from the db, I don't know what is the preferred solution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be an option to make it less awkward: #14935 (comment)

... and just retry.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors in retrieving the data from the db will cause the notification to fail, and it'll be automatically retried later.
If we have a persistent issue which causes the notifications to exceed their retry limit (5 by default) then that's actually a good signal that something is wrong.

@@ -8,7 +8,7 @@
<body style="margin: 0; padding: 0; font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617; background: #f8fafc;">
<div style="max-width: 600px; margin: 20px auto; padding: 60px; border: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-align: left; font-size: 14px; line-height: 1.5;">
<div style="text-align: center;">
<img src="https://coder.com/coder-logo-horizontal.png" alt="Coder Logo" style="height: 40px;" />
<img src="{{ .Labels._logo_url }}" alt="Company Logo" style="height: 40px;" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we present also the company name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤓 Will add it and present some screenshots so we can iterate with the name.

"c": "d",
"a": "b",
"c": "d",
"_logo_url": notifications.NotificationsDefaultLogoURL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the reason for exposing NotificationsDefaultLogoURL is just this test, I think you can safely switch to a generic http://foobar.tld/logo.png.

Copy link
Contributor Author

@defelmnq defelmnq Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was indeed not making sense at the time of your comment - good catch. Now that I changed to have these values defined in cli/server.go I need these. Anyway, I feel like it should maybe not be only related to notifications. Any idea about where I can put this kind of default values ?

Copy link
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Left some feedback, but I don't have much context on decisions around this, so feel free to ignore if my feedback isn't appropriate.

@dannykopping
Copy link
Contributor

You're totally right - I just have a question as we recommend using a 3:1 aspect ratio image (We recommend a transparent image with 3:1 aspect ratio.)

We set height: 40px now, which means an image with a 3:1 aspect ratio would be 120px wide.
The logo we use is 364:81 (~4.5:1) ratio, so with the height of 40px it becomes ~180px wide.

Current:

image

Simulated with image with 3:1 aspect ratio:

image

I think a logo with 3:1 would look quite bad (or at least, it wouldn't look like a header image; it looks quite wimpy now).

Perhaps we should use a logo of ours which is actually 3:1 and set the height a bit higher (60px) so we maintain this 180px width?

Copy link
Contributor

@dannykopping dannykopping left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nearly there, good work!

logoURL = NotificationsDefaultLogoURL
}

payload.Labels["_logo_url"] = logoURL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super 👍

Copy link
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for the background contexts and potential for locking up through stalled DB queries, I think this looks great, nice work!

cli/server.go Outdated
@@ -1309,6 +1309,30 @@ func templateHelpers(options *coderd.Options) map[string]any {
return map[string]any{
"base_url": func() string { return options.AccessURL.String() },
"current_year": func() string { return strconv.Itoa(time.Now().Year()) },
"logo_url": func() string {
logoURL, err := options.Database.GetLogoURL(context.Background())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps passing a context to templateHelpers (server lifetime) would be a good idea, and perhaps we should define a maximum execution time here (context.WithTimeout) so that we don't end up in a situation where the DB query simply blocks for an indefinite amount of time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

I have similar feelings. We might be risking by placing DB call inside a template helper. I'm wondering if we shouldn't implement something similar to AppearanceFetcher for the site handler.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed thanks, good catch ! 🙏

As these methods seem directly connected to templating engine from go , I've not been able to find a way to pass a context to it. Anyway, I created a context with a hardcoded 1 second timeout for it , to at least make it safer.

Not sure if there's a better way to do ?

Copy link
Member

@mtojek mtojek Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you say if we slightly modify this concept. My main concern is around swallowing the error in favor of returning an empty string "", so:

Wherever we render the htmlBody:

	htmlBody, err = render.GoTemplate(htmlTemplate, payload, s.helpers)
	if err != nil {
		return nil, xerrors.Errorf("render full html template: %w", err)
	}

we create a dynamic template.FuncMap with app_name and logo_url. We could add a reference to the store to SMTPHandler, and retrieve values before rendering.

Thoughts?

@github-actions github-actions bot added the stale This issue is like stale bread. label Oct 12, 2024
@matifali matifali removed the stale This issue is like stale bread. label Oct 12, 2024
@defelmnq defelmnq force-pushed the feat/notif-custom-logo branch from e611e57 to 73e07e9 Compare October 16, 2024 12:29
@defelmnq
Copy link
Contributor Author

Update here - based on the last conversation with the team I moved the fetchApplicationName and fetchLogoURL functions to be isolated.

Also generated a specific golden file for a notification with custom application name and logo URL so we can validate they are correctly fetched.

After discussing it with @dannykopping I changed the Dispatcher signature to take dynamic helpers instead of ones defined at instantiation.

Finally, in term of logic we have three paths :

  • Either the appearance settings are not defined and we take Coder values.
  • Either they are defined and we take it.
  • Either we have a DB error while trying to fetch it and we go to the error path without sending the notification.

@defelmnq defelmnq requested a review from mtojek October 18, 2024 13:08
Copy link
Member

@mtojek mtojek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ship it 👍

Copy link
Contributor

@dannykopping dannykopping left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nearly there!

}

func (s *SMTPHandler) Dispatcher(payload types.MessagePayload, titleTmpl, bodyTmpl string) (DeliveryFunc, error) {
func (s *SMTPHandler) Dispatcher(helpers template.FuncMap, payload types.MessagePayload, titleTmpl, bodyTmpl string) (DeliveryFunc, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: when passing arguments, I try think of them in order of significance.
The helpers are not the most significant argument here; they're likely the least. I'd move helpers to the end.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

@@ -8,7 +8,7 @@
<body style="margin: 0; padding: 0; font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617; background: #f8fafc;">
<div style="max-width: 600px; margin: 20px auto; padding: 60px; border: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-align: left; font-size: 14px; line-height: 1.5;">
<div style="text-align: center;">
<img src="https://coder.com/coder-logo-horizontal.png" alt="Coder Logo" style="height: 40px;" />
<img src="{{ logo_url }}" alt="{{ app_name }} Logo" style="height: 40px;" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: my immediate thought was to use a Data URL but apparently support for this is extremely limited.

@defelmnq defelmnq requested a review from dannykopping October 18, 2024 14:36
Copy link
Contributor

@dannykopping dannykopping left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

We just need to ensure failures to fetch app name / logo will be retried, then we can get this merged.

if err != nil {
return nil, xerrors.Errorf("fetch app name: %w", err)
return nil, xerrors.Errorf("fetch helpers: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you validated that failures here cause a "temporary failure" so that subsequent requests are retried?

@stirby
Copy link
Collaborator

stirby commented Oct 18, 2024

Exciting!

@defelmnq defelmnq merged commit 297089e into main Oct 22, 2024
26 checks passed
@defelmnq defelmnq deleted the feat/notif-custom-logo branch October 22, 2024 12:06
@github-actions github-actions bot locked and limited conversation to collaborators Oct 22, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Notifications: allow customers to rebrand email
7 participants