feat(destinations): add Resend and SendGrid email destinations (JITSU-71)#1377
Conversation
7441557 to
8f017b6
Compare
8f017b6 to
d481e86
Compare
There was a problem hiding this comment.
Reviewed the new Resend/SendGrid destination functions, shared contact helpers, and console wiring. I focused on contact upsert/reconciliation paths, error handling, and membership bookkeeping behavior. I found two additional correctness risks around silently skipping connector-managed bookkeeping fields after API errors; details are in inline comments.
d481e86 to
9975f21
Compare
There was a problem hiding this comment.
Reviewed the new Resend/SendGrid destination implementations, shared contacts helpers, and the console/meta wiring for the new destination types. I focused on reconciliation flow and retry/error semantics. I left a few inline comments for correctness risks that can leave stale memberships or drop retryable failures.
…-71) Sync users to Resend and SendGrid as contacts. identify() upserts the contact and reconciles its audience/list membership; track/page/group update contact fields only. Audiences and lists are configured by name (comma-separated, auto-created), and membership is reconciled to the desired set — audiences the connector added that are no longer listed are removed. Contacts are keyed by email; userId and anonymousId are stored as properties.
9975f21 to
3311e07
Compare
There was a problem hiding this comment.
Reviewed the new Resend/SendGrid destination implementations, shared contacts helpers, destination registration, and console schema wiring. I found two actionable issues: one retry-classification gap in SendGrid field-definition fallback and one bookkeeping-correctness risk in Resend when internal managed-audience property definition fails with permanent 4xx.
|
few autoreview comments left. looks a bit complicated but at the same time quite flexible from the beginning |
Address remaining auto-review findings and align the two connectors: - SendGrid ensureCustomField: the fallback field_definitions GET (run when the POST fails with a permanent 4xx) was outside classifyError, so a transient 429/5xx there escaped as a raw JsonFetchError and Rotor would drop the event. Wrap it so transient failures become RetryError. - Both connectors: a permanent 4xx while defining the *internal* jitsu_managed_audiences / jitsu_managed_lists marker was silently skipped, letting membership mutate while reconciliation bookkeeping was lost. The skip now applies to user-supplied keys only; failing to define the internal marker fails/surfaces the event. - SendGrid: extract a dedicated updateContactFromGroup() (mirroring Resend) and a shared applyContactUpdate() helper, so group handling is organized identically across both connectors. Group events now also skip when groupId is missing, matching Resend. No behavior change for track/page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
On the Also pushed fixes for the two remaining auto-review threads (SendGrid refetch retry gap; internal managed-marker never skipped on permanent 4xx — both connectors). Typecheck + |
There was a problem hiding this comment.
Reviewed range 185aa52..7b015ee with focus on correctness/retry behavior in the new Resend/SendGrid destinations, shared contacts helpers, and console wiring.
I did not find additional actionable bugs or security regressions in the current head state. The latest changes address the earlier retry/reconciliation hazards discussed in existing threads (including deferred managed-marker writes and transient-error classification).
Note: local test execution was not possible in this environment because is unavailable.
|
@vklimontovich I suggest that by default Jitsu must only add audiences and |
|
@absorbb good concern, but this specific damage can't happen — Jitsu already tracks what it manages and never touches anything else. Here's how it works: On every So an audience that was not added by Jitsu — a manually managed list, another tool, another Jitsu connection — is never in Your example: user A is in The only case Jitsu removes someone from an audience is when Jitsu itself previously added them and that audience later drops out of the desired set — e.g. config was Net: 'remove from all unknown audiences' doesn't happen — unknown (non-Jitsu-managed) audiences are exactly the ones we never remove. If on top of that you'd also like to drop the reconcile-on-omission behavior and make it purely additive (removal only via explicit |
|
@vklimontovich
I would prefer this approach |
|
🚀 |
What
Adds two cloud destinations that sync Jitsu users into email/marketing tools as contacts:
libs/destination-functions/src/functions/resend-destination.tslibs/destination-functions/src/functions/sendgrid-destination.tsPart of JITSU-71. Postmark was dropped — it has no contacts/audiences API (transactional-only); see the Linear issue.
Behavior
identifyupserts the contact (create or update, keyed by email), stores name + all other traits asproperties/custom fields, and reconciles audience/list membership to the desired set.
track/page/screenupdate an existing contact's fields only (never create); driven by fieldsexplicitly set on the event.
groupfolds the group id + traits intogroup_*properties.Audiences / lists by name, with reconciliation
A,BthenB,Cremoves the contact fromA. To do this without a Jitsu-sidecache, the connector records the set it manages on the contact itself (
jitsu_managed_audiences/jitsu_managed_lists), so it only ever removes memberships it added — never ones added manually or by other tools.resendAudiences/sendgridListstrait.Identity
userId/anonymousIdare stored as properties/custom fields.resolveEmailFromUserIdflag cachesuserId → emailso events carrying only a userId can match.Code organization
src/functions/lib/contacts.ts(trait parsing, email resolution,managed-set encode/decode, target-name resolution, error classification). Each connector keeps its own API
I/O — the upsert models differ (Resend
GET-then-POST/PATCH+ segment endpoints; SendGrid asyncPUT+field-definition management + list
DELETE), so a shared base class would leak those differences.__tests__/lib/mem-store.ts(createMemoryStore) and__tests__/lib/email-contacts-test-data.ts(emailContactTestEvents).apiBasecredential on both (SendGrid still honors theGlobal/EU region when unset).
Testing
Both destinations were verified live against real accounts (create, update-no-duplicate, multi-audience
reconciliation with removal, custom properties, cache-resolved updates, track-never-creates). Env-gated vitest
tests included (
TEST_RESEND_DESTINATION_CONFIG/TEST_SENDGRID_DESTINATION_CONFIG); they skip without config.pnpm --filter @jitsu/destination-functions typecheckand console typecheck pass.SendGrid-specific: contact import is asynchronous; a just-created custom field is deferred via
RetryErroruntilit propagates, and email is kept as the sole identifier (avoids SendGrid's "provide all identifiers" rejection).
Docs PR in @jitsucom/websites (jitsucom/websites#40).
🤖 Generated with Claude Code