-
Notifications
You must be signed in to change notification settings - Fork 877
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
Conversation
All contributors have signed the CLA ✍️ ✅ |
@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. |
I have read the CLA Document and I hereby sign the CLA |
@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 ( 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. |
coderd/notifications/notifier.go
Outdated
logoURL = NotificationsDefaultLogoURL | ||
} | ||
|
||
payload.Labels["_logo_url"] = logoURL |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super 👍
coderd/notifications/notifier.go
Outdated
@@ -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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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;" /> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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 ?
There was a problem hiding this 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.
We set Current: Simulated with image with 3:1 aspect ratio: 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? |
There was a problem hiding this 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!
coderd/notifications/notifier.go
Outdated
logoURL = NotificationsDefaultLogoURL | ||
} | ||
|
||
payload.Labels["_logo_url"] = logoURL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super 👍
There was a problem hiding this 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()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch 👍
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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?
… to notifications package
e611e57
to
73e07e9
Compare
… to notifications package
Update here - based on the last conversation with the team I moved the 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 :
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ship it 👍
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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;" /> |
There was a problem hiding this comment.
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.
...tions/testdata/rendered-templates/smtp/TemplateWorkspaceDeleted_CustomAppearance.html.golden
Show resolved
Hide resolved
There was a problem hiding this 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.
coderd/notifications/notifier.go
Outdated
if err != nil { | ||
return nil, xerrors.Errorf("fetch app name: %w", err) | ||
return nil, xerrors.Errorf("fetch helpers: %w", err) |
There was a problem hiding this comment.
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?
Exciting! |
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.