-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat(messaging): add message library with email templates #30783
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
base: master
Are you sure you want to change the base?
Conversation
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
…into broadcast-revive
…into broadcasts-delivery
…into messaging-campaigns
@@ -708,6 +708,9 @@ export const hogFunctionConfigurationLogic = kea<hogFunctionConfigurationLogicTy | |||
willChangeEnabledOnSave: [ | |||
(s) => [s.configuration, s.hogFunction], | |||
(configuration, hogFunction) => { | |||
if (configuration?.kind === 'messaging_template') { |
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.
🥴
posthog/api/message_templates.py
Outdated
permission_classes = [IsAuthenticated] | ||
|
||
serializer_class = MessageTemplateSerializer | ||
queryset = HogFunction.objects.all() |
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.
We may just want to make a separate table for this. Originally I figured we'd be making more use of pre-existing HogFunction duplicate/template functionality but we're really just doing hog function name, description, and input copying
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.
Yeah no way this should be a hog function. Hog functions should only be used for things that are actually runnable units of work, otherwise it just becomes a meaningless concept.
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.
PR Summary
This PR adds a messaging library with reusable email templates to PostHog's messaging platform. Here are the key points about the changes:
- Added new API endpoints
/messaging/messages
and/messaging/templates
with corresponding ViewSets for managing email templates and messages - Implemented email template editor with preview/edit modes using EmailEditor component and template selection functionality
- Added validation for email template inputs including required fields (from, subject, html) and proper error handling
- Created new messaging library UI components including TemplatesTable and MessagesTable for managing templates/messages
- Added proper team-based access control and read-only API endpoints with comprehensive test coverage
Key concerns to address:
- Template's hog script only prints 'Email template loaded' without actual functionality
- Hardcoded values like info@posthog.com and posthog.com links should be configurable
- Missing validation for person properties used in template strings
- Inconsistent parent query lookup parameters between project_id and team_id in API endpoints
- InputsItemSerializer validation gaps for the new email_template type
33 file(s) reviewed, 26 comment(s)
Edit PR Review Bot Settings | Greptile
frontend/src/products.tsx
Outdated
messagingLibraryTemplateFromMessage: (configuration?: object): string => | ||
`/messaging/library/templates/new?#configuration=${encodeURIComponent(JSON.stringify(configuration))}`, |
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.
logic: URL hash parameters (#configuration) won't be accessible via normal URL parameter parsing. Consider using a regular query parameter instead.
'/messaging/library': ['MessagingLibrary', 'messagingLibrary'], | ||
'/messaging/library/new': ['MessagingLibrary', 'messagingLibraryNew'], | ||
'/messaging/library/:id': ['MessagingLibrary', 'messagingLibraryTemplate'], | ||
'/messaging/library/templates/:id': ['MessagingLibraryTemplate', 'messagingLibraryTemplate'], |
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.
logic: Route pattern '/messaging/library/templates/:id' will match before '/messaging/library/templates/new', which could cause issues with the 'new' template route
props={props.formLogicProps} | ||
formKey={props.formKey} | ||
> | ||
{(emailMetaFields || ['from', 'to', 'subject']).map((field) => ( |
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.
style: emailMetaFields fallback could be defined as a constant to avoid recreation on each render
posthog/api/message_templates.py
Outdated
def retrieve(self, request: Request, *args, **kwargs): | ||
if self.get_object().kind != "messaging_template": | ||
return Response(status=status.HTTP_404_NOT_FOUND) | ||
return Response(self.get_serializer(self.get_object()).data) |
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.
style: Consider adding error handling for get_object() in case object doesn't exist
def retrieve(self, request: Request, *args, **kwargs): | |
if self.get_object().kind != "messaging_template": | |
return Response(status=status.HTTP_404_NOT_FOUND) | |
return Response(self.get_serializer(self.get_object()).data) | |
def retrieve(self, request: Request, *args, **kwargs): | |
obj = self.get_object() | |
if obj.kind != "messaging_template": | |
return Response(status=status.HTTP_404_NOT_FOUND) | |
return Response(self.get_serializer(obj).data) |
environments_router.register( | ||
r"messaging/messages", | ||
messages.MessageViewSet, | ||
"project_messaging", | ||
["project_id"], | ||
) |
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.
logic: Parent query lookup should be team_id
instead of project_id
since this is registered on the environments router
environments_router.register( | |
r"messaging/messages", | |
messages.MessageViewSet, | |
"project_messaging", | |
["project_id"], | |
) | |
environments_router.register( | |
r"messaging/messages", | |
messages.MessageViewSet, | |
"project_messaging", | |
["team_id"], | |
) |
const { templates, templatesLoading } = useValues(templatesLogic) | ||
const { deleteTemplate } = useActions(templatesLogic) | ||
|
||
const columns: LemonTableColumns<Message> = [ |
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.
style: Table uses Message type but this is a templates table - should define and use a Template type instead for better type safety
|
||
export const libraryTemplateLogic = kea<libraryTemplateLogicType>([ | ||
path(['products', 'messaging', 'frontend', 'libraryTemplateLogic']), | ||
key(({ id }) => id ?? 'unknown'), |
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.
logic: Using 'unknown' as fallback for missing ID could lead to key collisions if multiple instances have missing IDs. Consider using a UUID or timestamp-based fallback.
if (!id) { | ||
return [] | ||
} |
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.
logic: Early return with empty array when id is null could cause UI inconsistency. Consider showing at least the base messaging library breadcrumbs.
created_at: string | null | ||
name: string | ||
description: string | ||
content: Record<string, any> |
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.
style: content field uses Record<string, any> which is too permissive. Consider defining a stricter type for the content structure.
products/messaging/manifest.tsx
Outdated
messagingLibraryTemplateFromMessage: (configuration?: object): string => | ||
`/messaging/library/templates/new?#configuration=${encodeURIComponent(JSON.stringify(configuration))}`, |
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.
style: Using '#' in query string for configuration may cause issues with URL parsing - consider using regular query parameter
📸 UI snapshots have been updated1 snapshot changes in total. 0 added, 1 modified, 0 deleted:
Triggered by this commit. |
posthog/api/message_templates.py
Outdated
permission_classes = [IsAuthenticated] | ||
|
||
serializer_class = MessageTemplateSerializer | ||
queryset = HogFunction.objects.all() |
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.
Yeah no way this should be a hog function. Hog functions should only be used for things that are actually runnable units of work, otherwise it just becomes a meaningless concept.
from posthog.api.shared import UserBasicSerializer | ||
|
||
|
||
class MessageSerializer(serializers.ModelSerializer): |
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 couldn't see anywhere what this actually is? It sort of just looks like a viewset for filtering hog functions... so why not just use the hog function api?
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.
No reason we can't use hog function API, just wanted to break ground on messaging APIs so that we start building up a cleaner interface around messaging that abstracts away hog function concepts
…into messaging-library
📸 UI snapshots have been updated1 snapshot changes in total. 0 added, 1 modified, 0 deleted:
Triggered by this commit. |
…into messaging-library
…into messaging-library
Problem
Adds Messaging library + re-usable emails to messaging platform
Changes
Screenity.video.-.Apr.15.2025.webm
Does this work well for both Cloud and self-hosted?
Yes
How did you test this code?
Added API tests for new backend functionality, working on an Playwright suite in background