Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5cf3e24
feat(node): send transactional email from a key or an HTML template file
Silthus Oct 17, 2026
c1c21e3
fix(node): retry an email send only after an answer that queued nothing
Silthus Oct 17, 2026
f5ff383
fix(node): read template file metadata in one scan that respects quot…
Silthus Oct 17, 2026
6a875b3
fix(node): scan template file metadata in linear time
Silthus Oct 17, 2026
31ba673
fix(node): skip the text of every title when reading a template file
Silthus Oct 17, 2026
9d302f4
fix(node): never let an automatic upload republish replaced content
Silthus Oct 17, 2026
3dc17bf
feat(node): record the repository, path and commit of an uploaded tem…
Silthus Oct 17, 2026
d1f4c6b
fix(node): read only small regular git files and parse git config com…
Silthus Oct 17, 2026
2bb844f
feat(node): send a PostHog template by id with idempotent retries
Silthus Oct 22, 2026
5256ccb
feat(node): name a paused workflow and say what a replayed key still …
Silthus Oct 23, 2026
b56f913
chore(node): regenerate the API references
Silthus Oct 23, 2026
195dbf5
fix(node): refuse bad email options and retry an answer that broke off
Silthus Oct 23, 2026
5cb55a8
fix(node): refuse an email option the SDK doesn't take, and keep a re…
Silthus Oct 23, 2026
3b3e05f
feat(node): refuse an email send after shutdown with client_closed
Silthus Oct 24, 2026
7d59ad2
feat(node): send email content from code, uploading local templates b…
Silthus Oct 22, 2026
47d7e79
feat(node): export the code form types of emails.send
Silthus Oct 22, 2026
b7a2835
fix(node): send a local template to the recipient it was called with
Silthus Oct 22, 2026
ea1e2e7
docs(node): say that HTML with no text sends without a text part
Silthus Oct 23, 2026
61d2670
chore(node): regenerate the API references
Silthus Oct 23, 2026
65fcbb9
fix(node): refuse code fields on a template send and copy a local sen…
Silthus Oct 23, 2026
ea0195f
feat(node): send cc, bcc, headers, tags and attachments with a code send
Silthus Oct 22, 2026
102260e
feat(node): name the field an older server refuses on a code send
Silthus Oct 23, 2026
c1d9488
chore(node): regenerate the API references
Silthus Oct 23, 2026
abeb197
fix(node): refuse cc, bcc, headers, tags and attachments on a templat…
Silthus Oct 23, 2026
c20c6b5
feat(node): read an email's status with emails.get
Silthus Oct 22, 2026
9582575
fix(node): name an empty emails.get id a validation_error
Silthus Oct 22, 2026
1f6d871
chore(node): regenerate the API references
Silthus Oct 23, 2026
fc5dc17
fix(node): refuse an emails.get id that can't be encoded, and name it…
Silthus Oct 23, 2026
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/core-request-json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@posthog/core': patch
---

Let SDKs built on core send JSON requests to the PostHog API through the shared retrying transport.
5 changes: 5 additions & 0 deletions .changeset/node-emails-code-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-node': minor
---

The code form of `posthog.emails.send()` takes `cc`, `bcc`, `headers`, `tags` and `attachments`. An attachment's `content` is a Buffer, a Uint8Array or a Base64 string. A Resend attachment with a `path` returns an `invalid_attachment` error, because PostHog can't fetch files. A PostHog server that doesn't take one of these fields yet refuses the send with an `invalid_request` error on that field, instead of sending without it.
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.
5 changes: 5 additions & 0 deletions .changeset/node-emails-get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-node': minor
---

Add `posthog.emails.get(id)` to read the latest status of an email that `posthog.emails.send()` queued.
5 changes: 5 additions & 0 deletions .changeset/node-emails-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-node': minor
---

Add `posthog.emails.send()` to send transactional email from a template made in PostHog, by its id. Every send carries an idempotency key and retries network errors, throttling and an unavailable pipeline with the same key. Results are `{ data, error }`, with Resend's error names. New client options: `appHost`, `maxRetries` and `migratedFrom`.
26 changes: 26 additions & 0 deletions packages/core/src/posthog-core-stateless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
PostHogFeatureFlagDetails,
FeatureFlagDetail,
SurveyResponse,
PostHogApiResponse,
PostHogFetchResponse,
PostHogFetchBodyBytes,
PostHogFetchOptions,
Expand All @@ -40,6 +41,7 @@ import {
createLogger,
getEventUuid,
safeJsonStringify,
trySafe,
} from './utils'
import { parseRetryAfterMs } from './utils/retry-after'
import { uuidv7 } from './vendor/uuidv7'
Expand Down Expand Up @@ -156,6 +158,11 @@ type SuccessfulWriteResponseHandling = {
type: 'successful-write'
}

async function readApiResponse(response: PostHogFetchResponse): Promise<PostHogApiResponse> {
const text = await response.text()
return { status: response.status, body: trySafe(() => JSON.parse(text)) }
}

export const maybeAdd = (key: string, value: JsonType | undefined): Record<string, JsonType> =>
value !== undefined ? { [key]: value } : {}

Expand Down Expand Up @@ -1874,6 +1881,25 @@ export abstract class PostHogCoreStateless {
return this._sendOtlpBatch({ path: 'traces', auth: 'bearer', payload })
}

protected async requestJson(
url: string,
options: PostHogFetchOptions,
retryOptions?: Partial<RetriableOptions>
): Promise<PostHogApiResponse> {
try {
return await this.fetchWithRetry(url, options, { type: 'required', consume: readApiResponse }, retryOptions)
} catch (error) {
if (error instanceof PostHogFetchHttpError) {
return {
status: error.status,
body: await error.json.catch(() => undefined),
retryAfterMs: error.retryAfterMs,
}
}
throw error
}
}

private fetchWithRetry<T>(
url: string,
options: PostHogFetchOptions,
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,13 @@ export type PostHogFetchResponse = {
body?: ReadableStream<Uint8Array> | null
}

export type PostHogApiResponse = {
status: number
body: unknown
/** The answer's `Retry-After` in milliseconds, when it sent a usable one. */
retryAfterMs?: number
}

export type PostHogQueueItem = {
message?: PostHogEventProperties
callback?: (err: unknown) => void
Expand Down
Loading