Skip to content

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

Open
wants to merge 337 commits into
base: master
Choose a base branch
from

Conversation

havenbarnes
Copy link
Contributor

@havenbarnes havenbarnes commented Apr 3, 2025

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

havenbarnes and others added 30 commits March 31, 2025 11:16
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>
@havenbarnes havenbarnes changed the title feat(messaging): add API support for message library feat(messaging): add message library with email templates Apr 15, 2025
@@ -708,6 +708,9 @@ export const hogFunctionConfigurationLogic = kea<hogFunctionConfigurationLogicTy
willChangeEnabledOnSave: [
(s) => [s.configuration, s.hogFunction],
(configuration, hogFunction) => {
if (configuration?.kind === 'messaging_template') {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🥴

@havenbarnes havenbarnes marked this pull request as ready for review April 15, 2025 22:19
permission_classes = [IsAuthenticated]

serializer_class = MessageTemplateSerializer
queryset = HogFunction.objects.all()
Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor

@greptile-apps greptile-apps bot left a 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

Comment on lines 185 to 186
messagingLibraryTemplateFromMessage: (configuration?: object): string =>
`/messaging/library/templates/new?#configuration=${encodeURIComponent(JSON.stringify(configuration))}`,
Copy link
Contributor

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.

Comment on lines 67 to +68
'/messaging/library': ['MessagingLibrary', 'messagingLibrary'],
'/messaging/library/new': ['MessagingLibrary', 'messagingLibraryNew'],
'/messaging/library/:id': ['MessagingLibrary', 'messagingLibraryTemplate'],
'/messaging/library/templates/:id': ['MessagingLibraryTemplate', 'messagingLibraryTemplate'],
Copy link
Contributor

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) => (
Copy link
Contributor

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

Comment on lines 60 to 63
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)
Copy link
Contributor

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

Suggested change
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)

Comment on lines +650 to +655
environments_router.register(
r"messaging/messages",
messages.MessageViewSet,
"project_messaging",
["project_id"],
)
Copy link
Contributor

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

Suggested change
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> = [
Copy link
Contributor

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'),
Copy link
Contributor

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.

Comment on lines 24 to 26
if (!id) {
return []
}
Copy link
Contributor

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>
Copy link
Contributor

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.

Comment on lines 54 to 55
messagingLibraryTemplateFromMessage: (configuration?: object): string =>
`/messaging/library/templates/new?#configuration=${encodeURIComponent(JSON.stringify(configuration))}`,
Copy link
Contributor

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

@posthog-bot
Copy link
Contributor

📸 UI snapshots have been updated

1 snapshot changes in total. 0 added, 1 modified, 0 deleted:

  • chromium: 0 added, 1 modified, 0 deleted (diff for shard 5)
  • webkit: 0 added, 0 modified, 0 deleted

Triggered by this commit.

👉 Review this PR's diff of snapshots.

permission_classes = [IsAuthenticated]

serializer_class = MessageTemplateSerializer
queryset = HogFunction.objects.all()
Copy link
Contributor

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):
Copy link
Contributor

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?

Copy link
Contributor Author

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

@posthog-bot
Copy link
Contributor

📸 UI snapshots have been updated

1 snapshot changes in total. 0 added, 1 modified, 0 deleted:

  • chromium: 0 added, 1 modified, 0 deleted (diff for shard 5)
  • webkit: 0 added, 0 modified, 0 deleted

Triggered by this commit.

👉 Review this PR's diff of snapshots.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants