Repository navigation
Conversation
This was referenced Oct 22, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es and comments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The upload after `template_outdated` now sets `if_not_superseded`. When a newer deploy replaced the file's content between the send and the upload, PostHog changes nothing and the one resend returns the `template_superseded` warning. `uploadTemplate` still publishes replaced content, so it stays the rollback path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plate file An upload of a template file now sends source.repository, source.path and source.ref without configuration. Each part comes from the explicit source option, then from GitHub Actions, GitLab CI, Vercel or Railway variables, then from the nearest git checkout above the file. The checkout is read as files, without running git, and credentials in a remote URL never leave the process. Detection never fails a send. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ments Source detection read any file named like a git file, so a pipe in a .git directory could hang a send. It now reads only regular files up to 4 MiB. A Vercel provider named like an object property, such as "constructor", no longer throws, and a quoted origin URL followed by a comment is read the way git reads it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthog.emails.send now takes the flat template form of the shared SDK contract: templateId with templateData, posted to the template send route. Every send carries an Idempotency-Key, a UUID v4 unless the caller gives one, and retries network errors, an in-flight key, throttling and 503 with the same key. A monthly quota is never retried. Errors carry Resend's names, why and fix. New client options appHost, maxRetries and migratedFrom. The secret key only goes to the app host: derived for PostHog Cloud, required for any other host. The unreleased template field, templateFile() and uploadTemplate() go away. The content hash stays for local templates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…refuses A send to a workflow that isn't active answers 400 transactional_email_paused, a validation_error. The idempotency key docs now say a replay survives a paused workflow or a changed default sender, and still refuses a deleted or changed template or a project with no default sender (PostHog/posthog#120921). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A send with options that are not an object returns invalid_form instead of throwing. - A non-string idempotencyKey returns invalid_idempotency_key. - A maxRetries that is not a whole number from 0 falls back to the client option, then to 2, so NaN or Infinity can't retry forever. - A queued 202 whose body breaks off while it is read is a network error, so the send retries with the same key and returns the first id. - A network error keeps its cause in the message. - The secretKey docs say to turn off local evaluation for a key that can't read feature flags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 of 5 tasks
Silthus
force-pushed
the
feat/node-emails-code-form
branch
from
October 23, 2026 14:32
5193c5f to
1434877
Compare
…fused connection's cause - A send with an option posthog-node doesn't take, such as Resend's scheduledAt, returns invalid_form on that field instead of sending without it. - A refused connection to a host with two addresses keeps both causes in the network error message. - Core parses a JSON answer with trySafe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
force-pushed
the
feat/node-emails-code-form
branch
from
October 23, 2026 14:53
1434877 to
b9b52ca
Compare
Author
|
Note 🤖 Claude Opus 5.5 responding on behalf of Michael Decisions from the QA swarm rounds on the restacked stack.
|
A send after shutdown(), or while it drains, returns client_closed, named application_error, before any request, as the shared contract's 2026-10-24 entry asks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y hash posthog.emails.send takes the code form of the shared SDK contract: templateKey, subject, html and/or text, with optional from, replyTo and captureEngagement. The types refuse a send that mixes the two forms. Without templateData the content is rendered and goes to the send route as is, never uploaded. With templateData it is a local template: the SDK sends its content hash, and on 409 template_outdated uploads it by key with if_code_managed and sends once more with the same idempotency key. An upload refused because the key was edited in PostHog returns its error and is never retried. Template sends by id move to the send route as template.id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local template send read its options after hashing the content, so a caller that changed the same options object before the send finished sent to the new recipient. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d's data - A template send with from, replyTo, subject, html or text returns invalid_form on that field, instead of sending without it. - A code send without a subject, or without html or text, returns invalid_form before any request. - A local template send copies its request body before it hashes the content, so a caller that changes templateData during the send changes nothing. - The round-trip tests expect a fixed content hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
force-pushed
the
feat/node-emails-code-form
branch
from
October 24, 2026 12:43
b9b52ca to
65fcbb9
Compare
6 of 23 tasks
This was referenced Oct 24, 2026
Draft
This was referenced Oct 25, 2026
Silthus
added a commit
to Silthus/posthog-js
that referenced
this pull request
Oct 25, 2026
A code send with templateData sends template: { key, hash }. When PostHog
answers template_outdated, the SDK uploads the content by key and sends again
once with the same idempotency key. Moves the local template arm of
PostHog#5371, with its content hash module.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
An app that renders its own emails, or keeps a template with variables in its code, cannot send through posthog-node:
emails.send()takes only a template made in PostHog. A Resendhtml:or React Email call site needs a hand conversion first.This PR adds the code form of the shared SDK contract PostHog/posthog#120921. The app names the email with a
templateKey, and the SDK keeps a local template in sync by its content hash. Nobody calls an upload.Slice 10 of strand Silthus/posthog#536, map Silthus/posthog#461. Its extra fields stack on it (#5373), then
emails.get(#5372).Stacked on #5312 (slice 09). This fork PR targets
main, so its diff includes #5312. This layer alone:feat/node-emails-send...feat/node-emails-code-form.Server dependency, not merged: the code form and
template: { id }onPOST messaging/send/email/,409 template_outdatedfor an unknown key, andif_code_managedon the upload come from PostHog/posthog#121272 on #118924. This PR is built against the contract and PostHog/posthog#121272's routes at the HTTP seam. The send route honors theIdempotency-Keyonly after slice 06 of the send-api strand.Impact
Pays into Grow Workflows through billed
workflows_emailsfrom API sends, read as orgs that send from code and orgs that switched (not measured yet: the send attribution of PostHog/posthog#120628 plus its insight). In scope: Messaging, email. Expected move: medium, up: this is the form a Resend call site moves to. Run cost: n/a.Merge Danger
Caution
🔴 Merge risk: high · one-way door (merge-risk rubric)
posthog.emails.send()contract with the code form, moves the template form tomessaging/send/email/, and uploads local templates to PostHog by key on409 template_outdated. Once released, the field names and the upload rule are a contract, and a send delivers a real email.if_code_managedandif_not_superseded, so it never overwrites a template edited in PostHog or newer content. A JS caller that mixes the forms getsinvalid_formbefore any request. Tests at the HTTP boundary pin every body; not yet run against a live server (gap below).Changes
templateKey,subject,htmland/ortext, optionalfrom,replyTo(a string becomes a one-item list) andtemplateData.captureEngagementapplies to both forms.subjector withouthtmlandtext, and code fields on the template form. At runtime, before any request, a JS caller getsinvalid_formfor a mixed send. It getsinvalid_formwith thefieldforfrom,replyTo,subject,htmlortexton the template form (it would drop them), for a code form without a stringsubjector withouthtmlortext, and for an option neither form takes, such as Resend'sscheduledAt. A badtemplateKeygetsinvalid_template_key.templateData):POST messaging/send/email/withtemplate_key,subject,html,text,from,reply_to. Notextis derived in the SDK: PostHog derives it, and sends HTML with no text in it, such as a single image, without a text part. The docs say so.templateData): the SDK copies the request body, hashessubject,htmlandtextwith the content hash (v1), and sendstemplate: { key, hash }withmessage_data. A caller that changestemplateDataor the options while the hash runs changes nothing. On409 template_outdatedit uploads by key (PUT messaging_templates/by_key/{key}/withname: <key>, the content,if_code_managed: true,if_not_superseded: true, and no source) and sends once more with the same idempotency key.409 template_edited_in_posthogfrom the upload returns as the error and is never retried. A secondtemplate_outdatedreturns as the error.messaging/send/email/astemplate: { id }. No caller change.Parity with the contract and posthog-python
to,template_key,subject,html,text,from,reply_to[],capture_engagement,identifiers.distinct_idto,template: { key, hash },message_data,from,reply_to[],identifierstemplate: { id },message_data,capture_engagementv1:SHA-256, length-prefixed subject, preheader, html, text, line endings normalized; the server's three test vectorsname: <templateKey>, content,if_code_managed,if_not_superseded, nosource409 template_outdated, then one resend with the same key and body409returned asvalidation_error, no retryJSON.stringify, compact, UTF-8Evidence
emailsnamespace on a real client,fetchmocked byFakeEmailsApi, which now speaks the send route (template by id, rendered content, local template with its hash rule) and the upload route withif_code_managed(emails.spec.ts).content_hashv1 test vectors, the same three thattest_content_hash.pyon feat(messaging): upload, send and lock code-managed email templates posthog#118924 asserts (email-content-hash.spec.ts). The round-trip test expects a fixed hash, computed with posthog-python's rule, not with the SDK's helper.tscwith@ts-expect-errorcases (__tests__/types/emails-forms.ts).409with no send; a secondtemplate_outdatedreturned; two local sends that reuse one options object and change itstoandtemplateData, each reaching the recipient with the data it was called with;invalid_formwith itsfieldfor each code field on the template form, for a code form without a subject or content, and forscheduledAton either form.65fcbb97: nodevitest(1325 passed), the edge build tests (1325 passed),tscfor the build and the type tests,oxlint,oxfmt, build andgenerate-references; corevitest.html: covered by the same live run.Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Michael (@Silthus) decided the two code cases and the upload rule in the spec. Built by Claude Code (Claude Opus 5.5) test-first, as one build lane of the map.
templateData: absent sends as is, present sends a hash and uploads ontemplate_outdatedif_code_managedper the contract, plusif_not_superseded, so an older deploy never republishes replaced contentnametemplateKey, as posthog-python doesscheduledAtinvalid_formon its field before any request, as #5312 does, instead of being dropped; a list of Resend-only names would miss typos and future fieldsinvalid_formon the field, not droppedOpen: none beyond #5312's. The text-only upload is a server fix in the send-api strand.
Involvement: spec decisions by Michael; one strand review by GPT-6.1 Sol with a final read; a QA swarm over the restacked stack in two rounds (Review lane GPT-6.1 Sol with seven lenses, Fresh-eyes lane Claude Opus 5.5) with its fixes in this PR; no live run yet.
🤖 Generated with Claude Code