Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/node-emails-code-form.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-node': minor
---

`posthog.emails.send()` also sends email content from your code, named by a `templateKey`: rendered content as is, or a local template that PostHog fills with `templateData`. The SDK uploads a local template on its first send and whenever its content changes. Template sends by id move to the same send route, with no change for callers.
170 changes: 132 additions & 38 deletions packages/node/references/posthog-node-references-latest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2035,6 +2035,43 @@
"path": "../core/src/error-tracking/types.ts",
"example": "Record<string, string>"
},
{
"id": "CodeEmailFields",
"name": "CodeEmailFields",
"properties": [
{
"description": "A verified sender, such as `'Acme <security@acme.com>'`. Without it, the project's default sender.",
"type": "string",
"name": "from"
},
{
"type": "string | string[]",
"name": "replyTo"
},
{
"type": "string",
"name": "subject"
},
{
"description": "Leave it out to send rendered content: the email goes out as is, and PostHog never stores it. Pass it to send a local template: PostHog fills its `{{ variables.* }}`, and the SDK uploads the template when PostHog does not hold its content yet.",
"type": "Record<string, unknown>",
"name": "templateData"
},
{
"description": "Names the email, such as `'password-reset'`, so its metrics, logs and status group under it in PostHog.",
"type": "string",
"name": "templateKey"
}
],
"path": "src/emails.ts"
},
{
"id": "CodeEmailForm",
"name": "CodeEmailForm",
"properties": [],
"path": "src/emails.ts",
"example": "CodeEmailFields & ({\n html: string;\n text?: string;\n} | {\n html?: string;\n text: string;\n})"
},
{
"id": "Coercer",
"name": "Coercer",
Expand Down Expand Up @@ -2113,7 +2150,7 @@
"name": "EmailError",
"properties": [
{
"description": "PostHog's error code, such as `invalid_recipient`, or one of the SDK's own: `missing_secret_key`, `missing_app_host`, `client_disabled`, `client_closed`, `network_error`, `api_error` for an answer without a code, `unexpected_error`, and `invalid_form`, `invalid_template_id` and `invalid_idempotency_key` for a send the SDK refused before any request.",
"description": "PostHog's error code, such as `invalid_recipient`, or one of the SDK's own: `missing_secret_key`, `missing_app_host`, `client_disabled`, `client_closed`, `network_error`, `api_error` for an answer without a code, `unexpected_error`, and `invalid_form`, `invalid_template_id`, `invalid_template_key` and `invalid_idempotency_key` for a send the SDK refused before any request.",
"type": "string",
"name": "code"
},
Expand Down Expand Up @@ -4303,17 +4340,29 @@
],
"path": "../core/src/utils/index.ts"
},
{
"id": "SendCodeEmailOptions",
"name": "SendCodeEmailOptions",
"properties": [],
"path": "src/emails.ts",
"example": "SendEmailBase & CodeEmailForm & Without<Omit<TemplateEmailForm, 'templateData'>>"
},
{
"id": "SendEmailBase",
"name": "SendEmailBase",
"properties": [
{
"description": "Whether PostHog captures the email's engagement events (sent, delivered, opened, clicked, bounced) as analytics events of the recipient. They count as analytics events. Without it, PostHog decides: on for the code form.",
"type": "boolean",
"name": "captureEngagement"
},
{
"description": "The recipient's distinct ID in your app. The email's events, such as sent, opened and clicked, land on this person and count as analytics events. Always pass it: without it, they land on a person keyed by the address.",
"type": "string",
"name": "distinctId"
},
{
"description": "Sends with a key PostHog already accepted queue nothing and return the first send's `id`, even after the email's workflow was paused or the default sender changed. A deleted template, or a project that no longer has a default sender, still refuses the send. Without it, the SDK makes a UUID v4 that covers its own retries. When a job runner retries the send, pass a key derived from what the email is for, such as `welcome:user_42`, and set `maxRetries: 0`.",
"description": "Sends with a key PostHog already accepted queue nothing and return the first send's `id`, even after the email's workflow was paused or the default sender changed. A deleted template, a local template whose content changed, or a project that no longer has a default sender while you name no `from`, still refuses the send. Without it, the SDK makes a UUID v4 that covers its own retries. When a job runner retries the send, pass a key derived from what the email is for, such as `welcome:user_42`, and set `maxRetries: 0`.",
"type": "string",
"name": "idempotencyKey"
},
Expand All @@ -4333,39 +4382,9 @@
{
"id": "SendEmailOptions",
"name": "SendEmailOptions",
"properties": [
{
"description": "The UUID of the template.",
"type": "string",
"name": "templateId"
},
{
"description": "Values for the template's `{{ variables.* }}`. The template can't read person properties.",
"type": "Record<string, unknown>",
"name": "templateData"
},
{
"description": "One recipient email address.",
"type": "string",
"name": "to"
},
{
"description": "The recipient's distinct ID in your app. The email's events, such as sent, opened and clicked, land on this\nperson and count as analytics events. Always pass it: without it, they land on a person keyed by the address.",
"type": "string",
"name": "distinctId"
},
{
"description": "Sends with a key PostHog already accepted queue nothing and return the first send's `id`, even after the email's\nworkflow was paused or the default sender changed. A deleted template, or a project that no longer has a default\nsender, still refuses the send. Without it, the SDK makes a UUID v4 that covers its own retries. When a job runner\nretries the send, pass a key derived from what the email is for, such as `welcome:user_42`, and set\n`maxRetries: 0`.",
"type": "string",
"name": "idempotencyKey"
},
{
"description": "How often to retry this send, a whole number from 0. Overrides the `maxRetries` client option.",
"type": "number",
"name": "maxRetries"
}
],
"path": "src/emails.ts"
"properties": [],
"path": "src/emails.ts",
"example": "SendTemplateEmailOptions | SendCodeEmailOptions"
},
{
"id": "SendEmailResult",
Expand Down Expand Up @@ -4416,14 +4435,65 @@
"name": "SendTemplateEmailOptions",
"properties": [
{
"description": "Values for the template's `{{ variables.* }}`. The template can't read person properties.",
"type": "Record<string, unknown>",
"name": "templateData"
"description": "One recipient email address.",
"type": "string",
"name": "to"
},
{
"description": "The recipient's distinct ID in your app. The email's events, such as sent, opened and clicked, land on this\nperson and count as analytics events. Always pass it: without it, they land on a person keyed by the address.",
"type": "string",
"name": "distinctId"
},
{
"description": "Sends with a key PostHog already accepted queue nothing and return the first send's `id`, even after the email's\nworkflow was paused or the default sender changed. A deleted template, a local template whose content changed, or\na project that no longer has a default sender while you name no `from`, still refuses the send. Without it, the SDK\nmakes a UUID v4 that covers its own retries. When a job runner retries the send, pass a key derived from what the\nemail is for, such as `welcome:user_42`, and set `maxRetries: 0`.",
"type": "string",
"name": "idempotencyKey"
},
{
"description": "Whether PostHog captures the email's engagement events (sent, delivered, opened, clicked, bounced) as analytics\nevents of the recipient. They count as analytics events. Without it, PostHog decides: on for the code form.",
"type": "boolean",
"name": "captureEngagement"
},
{
"description": "How often to retry this send, a whole number from 0. Overrides the `maxRetries` client option.",
"type": "number",
"name": "maxRetries"
},
{
"description": "The UUID of the template.",
"type": "string",
"name": "templateId"
},
{
"description": "Values for the template's `{{ variables.* }}`. The template can't read person properties.",
"type": "Record<string, unknown>",
"name": "templateData"
},
{
"description": "Names the email, such as `'password-reset'`, so its metrics, logs and status group under it in PostHog.",
"type": "never",
"name": "templateKey"
},
{
"type": "never",
"name": "subject"
},
{
"description": "A verified sender, such as `'Acme <security@acme.com>'`. Without it, the project's default sender.",
"type": "never",
"name": "from"
},
{
"type": "never",
"name": "replyTo"
},
{
"type": "never",
"name": "html"
},
{
"type": "never",
"name": "text"
}
],
"path": "src/emails.ts"
Expand Down Expand Up @@ -5073,6 +5143,23 @@
"path": "../core/src/types.ts",
"example": "(typeof SurveyWidgetType)[keyof typeof SurveyWidgetType]"
},
{
"id": "TemplateEmailForm",
"name": "TemplateEmailForm",
"properties": [
{
"description": "Values for the template's `{{ variables.* }}`. The template can't read person properties.",
"type": "Record<string, unknown>",
"name": "templateData"
},
{
"description": "The UUID of the template.",
"type": "string",
"name": "templateId"
}
],
"path": "src/emails.ts"
},
{
"id": "UnresolvedFlagReason",
"name": "UnresolvedFlagReason",
Expand All @@ -5094,6 +5181,13 @@
}
],
"path": "src/types.ts"
},
{
"id": "Without",
"name": "Without",
"properties": [],
"path": "src/emails.ts",
"example": "{\n [K in keyof T]?: never;\n}"
}
],
"categories": [
Expand Down
10 changes: 9 additions & 1 deletion packages/node/src/__tests__/email-content-hash.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ describe('emailContentHash', () => {
content: { subject: 'Grüße', preheader: 'Vorschau', html: '<p>ä</p>', text: 'ä' },
hash: 'v1:75cf6fc3189ca26f60da672d29a1d4c89c7db3dd458df3a550a34a4d2a92c149',
},
])('matches the spec test vector $hash', async ({ content, hash }) => {
{
content: {
subject: 'Welcome, {{ variables.first_name }}',
html: '<p>Hi {{ variables.first_name }}</p>',
text: '\r\n\r',
},
hash: 'v1:8112876ee1b2dd7327f571b04b546ed057bfff5a904d9f0ae507b6a449ab6453',
},
])('matches the content_hash v1 test vector of the server: $hash', async ({ content, hash }) => {
expect(await emailContentHash(content)).toBe(hash)
})

Expand Down
Loading
Loading