Skip to content

Commit 6a958d2

Browse files
authored
feat: add support for custom arguments in Twilio Sendgrid email notifications (#14879)
## Summary **What** — What changes are introduced in this PR? *Added support for passing SendGrid personalizations directly via provider_data in the notification payload.* **Why** — Why are these changes relevant or necessary? *This allows callers to pass any SendGrid-native personalization data (e.g. customArgs for analytics/event tracking, per-recipient overrides, dynamic template data per recipient) without the provider needing to know about each specific field.* **How** — How have these changes been implemented? *Modified the SendgridNotificationService.send() method to check notification.provider_data.personalizations. When present and non-empty, it is forwarded directly to the SendGrid send() call and the top-level to field is omitted (since personalizations handles recipient routing). When absent or empty, the existing behavior using the top-level to field is preserved unchanged.* **Testing** — How have these changes been tested, or how can the reviewer test the feature? *Unit tests added in integration-tests/__tests__/services.spec.ts using a mocked @sendgrid/mail. Three cases are covered: personalizations present and forwarded, absent (falls back to top-level to), and empty array (also falls back to top-level to).* resolves #15115 --- ## Examples Provide examples or code snippets that demonstrate how this feature works, or how it can be used in practice. This helps with documentation and ensures maintainers can quickly understand and verify the change. ```ts import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework" import { Modules } from "@medusajs/framework/utils" import { INotificationModuleService } from "@medusajs/framework/types" export default async function productCreateHandler({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { const notificationModuleService: INotificationModuleService = container.resolve(Modules.NOTIFICATION) const emailData = { name: "Sample Product", message: `New product created with id ${data.id}`, } const email = await notificationModuleService.createNotifications({ to: "xxxxx@xxxx.com", channel: "email", template: "xxxxxxxxxx", data: emailData, provider_data: { personalizations: [ { to: [{ email: "xxxxx@xxxx.com" }], customArgs: { customer_id: "cust_abc", ga4_id: "GA1.2.xxxxx", notification_type: "order_confirmation", }, }, ], }, }) } export const config: SubscriberConfig = { event: "product.created", } ``` --- ## Checklist Please ensure the following before requesting a review: - [x] I have added a **changeset** for this PR - Every non-breaking change should be marked as a **patch** - To add a changeset, run `yarn changeset` and follow the prompts - [x] The changes are covered by relevant **tests** - [x] I have verified the code works as intended locally - [x] I have linked the related issue(s) if applicable --- ## Additional Context Add any additional context, related issues, or references that might help the reviewer understand this PR. --- > [!NOTE] > **Low Risk** > Low risk: small, additive change to the SendGrid payload; main risk is passing unexpected `provider_data.custom_args` values, mitigated by coercing all values to strings. > > **Overview** > Adds support for SendGrid `customArgs` by reading `notification.provider_data.custom_args`, coercing values to strings, and including the result in the outgoing `sendgrid.send()` message payload. > > Includes a changeset bumping `@medusajs/notification-sendgrid` as a minor release to document the new capability. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 69b1ef0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
1 parent c18750f commit 6a958d2

3 files changed

Lines changed: 104 additions & 1 deletion

File tree

.changeset/forty-states-invent.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@medusajs/notification-sendgrid": patch
3+
---
4+
5+
feat(notification-sendgrid): add support for personalizations via provider_data

packages/modules/providers/notification-sendgrid/integration-tests/__tests__/services.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,100 @@
1+
import sendgrid from "@sendgrid/mail"
12
import { SendgridNotificationService } from "../../src/services/sendgrid"
3+
4+
jest.mock("@sendgrid/mail", () => ({
5+
__esModule: true,
6+
default: {
7+
setApiKey: jest.fn(),
8+
send: jest.fn().mockResolvedValue([{ statusCode: 202 }, {}]),
9+
},
10+
}))
11+
12+
jest.mock("@medusajs/framework/utils", () => ({
13+
AbstractNotificationProviderService: class {},
14+
MedusaError: class MedusaError extends Error {
15+
static Types = {
16+
INVALID_DATA: "invalid_data",
17+
UNEXPECTED_STATE: "unexpected_state",
18+
}
19+
type: string
20+
constructor(type: string, message: string) {
21+
super(message)
22+
this.type = type
23+
}
24+
},
25+
}), { virtual: true })
26+
27+
const mockSend = sendgrid.send as jest.MockedFunction<typeof sendgrid.send>
28+
229
jest.setTimeout(100000)
330

31+
describe("SendgridNotificationService - personalizations", () => {
32+
let service: SendgridNotificationService
33+
34+
beforeEach(() => {
35+
jest.clearAllMocks()
36+
service = new SendgridNotificationService(
37+
{ logger: console as any },
38+
{ api_key: "test-api-key", from: "sender@example.com" }
39+
)
40+
})
41+
42+
it("passes provider_data.personalizations directly to sendgrid and omits top-level to", async () => {
43+
await service.send({
44+
to: "recipient@example.com",
45+
channel: "email",
46+
template: "some-template",
47+
provider_data: {
48+
personalizations: [
49+
{
50+
to: [{ email: "recipient@example.com" }],
51+
customArgs: { campaign_id: "abc123", source: "welcome-flow" },
52+
},
53+
],
54+
},
55+
})
56+
57+
expect(mockSend).toHaveBeenCalledTimes(1)
58+
const message = mockSend.mock.calls[0][0] as any
59+
expect(message).not.toHaveProperty("to")
60+
expect(message.personalizations).toEqual([
61+
{
62+
to: [{ email: "recipient@example.com" }],
63+
customArgs: { campaign_id: "abc123", source: "welcome-flow" },
64+
},
65+
])
66+
})
67+
68+
it("uses top-level to and omits personalizations when provider_data.personalizations is absent", async () => {
69+
await service.send({
70+
to: "recipient@example.com",
71+
channel: "email",
72+
template: "some-template",
73+
})
74+
75+
expect(mockSend).toHaveBeenCalledTimes(1)
76+
const message = mockSend.mock.calls[0][0] as any
77+
expect(message.to).toEqual("recipient@example.com")
78+
expect(message).not.toHaveProperty("personalizations")
79+
})
80+
81+
it("falls back to top-level to when provider_data.personalizations is an empty array", async () => {
82+
await service.send({
83+
to: "recipient@example.com",
84+
channel: "email",
85+
template: "some-template",
86+
provider_data: {
87+
personalizations: [],
88+
},
89+
})
90+
91+
expect(mockSend).toHaveBeenCalledTimes(1)
92+
const message = mockSend.mock.calls[0][0] as any
93+
expect(message.to).toEqual("recipient@example.com")
94+
expect(message).not.toHaveProperty("personalizations")
95+
})
96+
})
97+
498
// Note: This test hits the sendgrid service, and it is mainly meant to be run manually after setting all the envvars below.
599
// We could also setup a sink email service to test this automatically, but it is not necessary for the time being.
6100
describe.skip("Sendgrid notification provider", () => {

packages/modules/providers/notification-sendgrid/src/services/sendgrid.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,17 @@ export class SendgridNotificationService extends AbstractNotificationProviderSer
7979
}
8080
}
8181

82+
const personalizations = notification.provider_data?.personalizations as
83+
| sendgrid.MailDataRequired["personalizations"]
84+
| undefined
85+
8286
const message: sendgrid.MailDataRequired = {
83-
to: notification.to,
8487
from: from,
8588
dynamicTemplateData: notification.data as
8689
| { [key: string]: any }
8790
| undefined,
8891
attachments: attachments,
92+
...(personalizations?.length ? { personalizations } : { to: notification.to }),
8993
...mailContent,
9094
}
9195

0 commit comments

Comments
 (0)