Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add data residency for eu and global regions #1390

Merged
merged 10 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion .github/workflows/test-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
timeout-minutes: 20
strategy:
matrix:
node: [ '6', '7', '8', '10', '12', '14', '16', 'lts' ]
node: [10, 12, 14, 16, lts]
env:
version: ${{ matrix.node }}
DOCKER_LOGIN: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_AUTH_TOKEN }}
Expand Down
38 changes: 38 additions & 0 deletions docs/use-cases/data-residency-set-hostname.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Choosing a hostname to send messages to
shrutiburman marked this conversation as resolved.
Show resolved Hide resolved

Use the `setDataResidency` setter to specify which host to send to:

Send to EU (hostname: `https://api.eu.sendgrid.com/`)
```js
const sgMail = require('@sendgrid/mail');
sgMail.setDataResidency('eu');
const msg = {
to: 'recipient@example.org',
from: 'sender@example.org',
subject: 'Hello world',
text: 'Hello plain world!',
html: '<p>Hello HTML world!</p>',
};
sgMail.send(msg);
```
Send to Global region, this is also the default host, if the setter is not used
(hostname: `https://api.sendgrid.com/`)
```js
const sgMail = require('@sendgrid/mail');
sgMail.setDataResidency('global');
const msg = {
to: 'recipient@example.org',
from: 'sender@example.org',
subject: 'Hello world',
text: 'Hello plain world!',
html: '<p>Hello HTML world!</p>',
};
sgMail.send(msg);
```

## Limitations

1. Setting the API Key (via `client.setApiKey()`) or Twilio Authentication (via `client.setTwilioEmailAuth()`) will override the hostname to default value. Use the setter call after this set-up.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this something we should figure out a better path for? This is not an ideal scenario.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tbischel @gladysmae08
Hi, I hope my PR offers a solution to this issue. Please take a look and I would appreciate any feedback.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this limitation is no longer necessary after the changes that were just added

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this won't introduce breaking changes for existing customers, right?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if customers don't have to change their code to achieve the same functionality, I think I'm good. if they do, we might want to look into a major version bump?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wont introduce a breaking change, as the region attribute that's newly introduced has a default value, and will only get updated if the setter for data residency is called.

2. Emails can only be sent to two hosts for now; 'eu' (https://api.eu.sendgrid.com/) and 'global' (https://api.eu.sendgrid.com/)
2. The default hostname is https://api.sendgrid.com/
3. The valid values for `region` in `client.setDataResidency(region)` are only `eu` and `global`. Case-sensitive.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"moment": "^2.19.3",
"sinon": "^2.3.2",
"sinon-chai": "^2.10.0",
"typescript": "^3.7.4"
"typescript": "^4.0.0"
},
"scripts": {
"lint": "if [ `node --version | cut -d'.' -f1 | cut -c 2` -ge \"8\" ]; then eslint . --fix; else echo \"eslint is not available for node < 8.0\"; fi",
Expand Down
27 changes: 25 additions & 2 deletions packages/client/src/classes/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ const API_KEY_PREFIX = 'SG.';
const SENDGRID_BASE_URL = 'https://api.sendgrid.com/';
const TWILIO_BASE_URL = 'https://email.twilio.com/';

// Initialize the allowed regions and their corresponding hosts
const REGION_HOST_MAP = {
eu: 'https://api.eu.sendgrid.com/',
global: 'https://api.sendgrid.com/',
};
class Client {
constructor() {
this.auth = '';
this.impersonateSubuser = '';
this.sendgrid_region = '';
shrutiburman marked this conversation as resolved.
Show resolved Hide resolved

this.defaultHeaders = {
Accept: 'application/json',
Expand All @@ -38,8 +44,10 @@ class Client {

setApiKey(apiKey) {
this.auth = 'Bearer ' + apiKey;
this.setDefaultRequest('baseUrl', SENDGRID_BASE_URL);

// this means that region was never set before
if (this.sendgrid_region == '') {
this.setDefaultRequest('baseUrl', SENDGRID_BASE_URL);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This technically works but I think it is unnecessary. We can just always reset the baseUrl here based on the saved region

this.setDefaultRequest('baseUrl', REGION_HOST_MAP[this.sendgrid_region]);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I have set the default region as 'global', we can reset based on that now. Thanks.

if (!this.isValidApiKey(apiKey)) {
console.warn(`API key does not start with "${API_KEY_PREFIX}".`);
}
Expand Down Expand Up @@ -94,6 +102,21 @@ class Client {
return this;
}

/**
* Global is the default residency (or region)
* Global region means the message will be sent through https://api.sendgrid.com
* EU region means the message will be sent through https://api.eu.sendgrid.com
**/
setDataResidency(region) {
if (!REGION_HOST_MAP.hasOwnProperty(region)) {
console.warn('Region can only be "global" or "eu".');
} else {
this.sendgrid_region = region;
this.setDefaultRequest('baseUrl', REGION_HOST_MAP[region]);
}
return this;
}

createHeaders(data) {
// Merge data with default headers.
const headers = mergeData(this.defaultHeaders, data);
Expand Down
5 changes: 5 additions & 0 deletions packages/client/src/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ declare class Client {
*/
setDefaultRequest<K extends keyof ClientRequest>(key: K | ClientRequest, value ?: ClientRequest[K]): this;

/**
* Sets the data residency as per region provided
*/
setDataResidency(region: string): this;

/**
* Create headers for request
*/
Expand Down
55 changes: 54 additions & 1 deletion packages/client/src/client.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
const nock = require('nock');

const sgClient = require('./client');
let testClient = require('./client');
const testRequest = (request, statusCode) => {
const sgClient = require('./client');
sgClient.setApiKey('SG.API Key');
Expand Down Expand Up @@ -3091,3 +3092,55 @@ describe('test_whitelabel_links__link_id__subuser_post', () => {
return testRequest(request, 200);
});
});

describe('setDataResidency', () => {
let consoleWarnSpy;
beforeEach(() => {
testClient = require('./client');
consoleWarnSpy = sinon.spy(console, 'warn');
});
afterEach(() => {
console.warn.restore();
});
it('should have default value of hostname as https://api.sendgrid.com/', () => {
expect(testClient.defaultRequest.baseUrl).to.equal('https://api.sendgrid.com/');
expect(testClient.sendgrid_region).to.equal('');
});
it('should send to host EU', () => {
testClient.setDataResidency('eu');
expect(testClient.defaultRequest.baseUrl).to.equal('https://api.eu.sendgrid.com/');
});
it('should send to host Global/default', () => {
testClient.setDataResidency('global');
expect(testClient.defaultRequest.baseUrl).to.equal('https://api.sendgrid.com/');
expect(testClient.sendgrid_region).to.equal('global');
});
it('should override the existing set hostname, if data residency setter is called after', () => {
testClient.setApiKey('SG.1234567890');
testClient.setDataResidency('eu');
expect(testClient.defaultRequest.baseUrl).to.equal('https://api.eu.sendgrid.com/');
});
it('should give a warning if the provided value is not allowed', () => {
testClient.setDataResidency('');
expect(consoleWarnSpy.calledOnce).to.equal(true);
});
it('should give a warning if the provided value is null', () => {
testClient.setDataResidency(null);
expect(consoleWarnSpy.calledOnce).to.equal(true);
});
it('setting the API Key wont reset the region set', () => {
testClient.setDataResidency('eu');
testClient.setApiKey('SG.1234567890');
gladysmae08 marked this conversation as resolved.
Show resolved Hide resolved
expect(testClient.defaultRequest.baseUrl).to.equal('https://api.eu.sendgrid.com/');
expect(testClient.sendgrid_region).to.equal('eu');
});
it('should send to host global and then call setApiKey', () => {
testClient.setDataResidency('global');
testClient.setApiKey('SG.1234567890');
expect(testClient.defaultRequest.baseUrl).to.equal('https://api.sendgrid.com/');
expect(testClient.sendgrid_region).to.equal('global');


});
});

Loading