Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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/e2e-smoke-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
# the real prod send path runs (outbox -> dispatch -> /api/transactional/send
# -> audit), but with Datamailer's dry_run flag the render is returned inline
# and nothing is delivered. For this to be SAFE, the dev DEPLOYMENT itself
# must run with DATAMAILER_TRANSACTIONAL_DRY_RUN=1 (set on the CMP dev ECS
# must run with RELAY_TRANSACTIONAL_DRY_RUN=1 (set on the CMP dev ECS
# task definition, provisioned via Terraform in DataTalksClub/infra-terraform
# -- it is not configured from this repo). Without it, dev would send real
# email during the smoke run; the email tests xfail if no audit appears.
Expand Down
48 changes: 24 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,30 +194,30 @@ Response:
In local development, the version comes from the `VERSION` environment
variable and falls back to `local-development-build-version-not-configured`.

## Datamailer
## Relay

The platform can sync created users and course enrollments to Datamailer.
The platform syncs users and course enrollments and sends email through Relay.

Set all required environment variables to enable the integration:

```bash
export DATAMAILER_URL="https://datamailer.dtcdev.click"
export DATAMAILER_API_KEY="<token>"
export DATAMAILER_CLIENT="dtc-courses"
export DATAMAILER_AUDIENCE="dtc-courses"
export DATAMAILER_FROM_EMAIL="courses"
export RELAY_URL="https://relay.dtcdev.click"
export RELAY_API_KEY="<token>"
export RELAY_CLIENT="dtc-courses"
export RELAY_AUDIENCE="dtc-courses"
export RELAY_FROM_EMAIL="courses"
```

Optional settings:

```bash
export DATAMAILER_STRICT="0"
export DATAMAILER_SYNC_ON_USER_CREATE="1"
export RELAY_STRICT="0"
export RELAY_SYNC_ON_USER_CREATE="1"
```

With `DATAMAILER_STRICT=0`, Datamailer API failures are logged and don't
break signup or enrollment flows. Set `DATAMAILER_STRICT=1` only when those
flows should fail on Datamailer errors.
With `RELAY_STRICT=0`, Relay API failures are logged and don't
break signup or enrollment flows. Set `RELAY_STRICT=1` only when those
flows should fail on Relay errors.

`PUBLIC_BASE_URL` is used for links in delivered transactional email. Set it
when sending email from a local server so messages contain public HTTPS links
Expand All @@ -230,17 +230,17 @@ export PUBLIC_BASE_URL="https://dev.courses.datatalks.club"
## Non-delivering email verification (dry run)

To exercise the transactional email path without delivering anything, set
`DATAMAILER_TRANSACTIONAL_DRY_RUN=1`. CMP then adds Datamailer's `dry_run` flag
`RELAY_TRANSACTIONAL_DRY_RUN=1`. CMP then adds Relay's `dry_run` flag
to every `POST /api/transactional/send`: the identical prod pipeline runs
(outbox -> dispatch -> send -> `DatamailerSendAudit`), but Datamailer renders
(outbox -> dispatch -> send -> `DatamailerSendAudit`), but Relay renders
the email and returns it inline without sending, queuing, or persisting
anything.
anything. `DatamailerSendAudit` is retained as a historical database model name;
it does not select or configure the old service.

The rendered subject/bodies land in the audit's `response_payload["rendered"]`,
which CMP exposes over HTTP at `GET /api/datamailer/send-audits`
(filter by `email` / `template_key` / `idempotency_key`). The e2e smoke suite
uses this to verify confirmation emails safely. This replaces the old local
Datamailer capture/testbed compose stack.
uses this to verify confirmation emails safely.

Deadline reminder emails are triggered by a short CMP management command:

Expand All @@ -254,8 +254,8 @@ task. The recurring trigger (an EventBridge/CloudWatch rule targeting
invocation role are provisioned via Terraform in `DataTalksClub/infra-terraform`,
alongside the CMP ECS service. CMP doesn't configure the schedule.

The scheduled task exits after reconciling Datamailer recipient lists and
triggering Datamailer list sends. Datamailer handles per-recipient delivery
The scheduled task exits after reconciling Relay recipient lists and
triggering Relay list sends. Relay handles per-recipient delivery
asynchronously.

Every event is attempted even if an earlier one fails; each failure is written
Expand All @@ -268,16 +268,16 @@ curl -s -H "Authorization: Token $TOKEN" \
```

Bulk sends post the whole recipient list inline, so they use a longer HTTP
timeout than transactional sends (`DATAMAILER_TIMEOUT_SECONDS`, default 60).
Don't lower it: hanging up mid-request makes Datamailer abandon the dispatch it
timeout than transactional sends (`RELAY_TIMEOUT_SECONDS`, default 60).
Don't lower it: hanging up mid-request makes Relay abandon the dispatch it
is partway through, stranding every not-yet-sent message at `queued` with no
error and no retry, while CMP records a failure for work that partly succeeded.

Datamailer transactional template keys are stable code-level constants in CMP.
Relay transactional template keys are stable code-level constants in CMP.
Don't configure one environment variable per template.

The CMP/Datamailer integration, including the conceptual design and API
reference, is documented in
The CMP/Relay integration, including the conceptual design and API reference,
is documented in the historically named
[`docs/datamailer-integration.md`](docs/datamailer-integration.md).

## API Data Access
Expand Down
4 changes: 2 additions & 2 deletions accounts/tests_account_settings_base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from django.test import override_settings

from accounts.tests_base import (
DATAMAILER_DISABLED_SETTINGS,
RELAY_DISABLED_SETTINGS,
AccountCourseTestCase,
)


@override_settings(**DATAMAILER_DISABLED_SETTINGS)
@override_settings(**RELAY_DISABLED_SETTINGS)
class AccountSettingsViewTestBase(AccountCourseTestCase):
def account_settings_profile_payload(self):
payload = {
Expand Down
10 changes: 5 additions & 5 deletions accounts/tests_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from accounts.models import CustomUser
from courses.models import Course, Enrollment

DATAMAILER_DISABLED_SETTINGS = {
"DATAMAILER_URL": "",
"DATAMAILER_API_KEY": "",
"DATAMAILER_CLIENT": "",
"DATAMAILER_AUDIENCE": "",
RELAY_DISABLED_SETTINGS = {
"RELAY_URL": "",
"RELAY_API_KEY": "",
"RELAY_CLIENT": "",
"RELAY_AUDIENCE": "",
}


Expand Down
4 changes: 2 additions & 2 deletions accounts/tests_email_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

from accounts.models import CustomUser
from accounts.tests_base import (
DATAMAILER_DISABLED_SETTINGS,
RELAY_DISABLED_SETTINGS,
AccountCourseTestCase,
)


@override_settings(**DATAMAILER_DISABLED_SETTINGS)
@override_settings(**RELAY_DISABLED_SETTINGS)
class AccountEmailPreferencesTestCase(AccountCourseTestCase):
@patch(
"accounts.views.email_preferences."
Expand Down
4 changes: 2 additions & 2 deletions accounts/tests_enrollment_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
from django.urls import reverse

from accounts.tests_base import (
DATAMAILER_DISABLED_SETTINGS,
RELAY_DISABLED_SETTINGS,
AccountCourseTestCase,
)


@override_settings(**DATAMAILER_DISABLED_SETTINGS)
@override_settings(**RELAY_DISABLED_SETTINGS)
class EnrollmentProfileTestCase(AccountCourseTestCase):
def enrollment_payload(self, display_public_profile=False):
payload = {
Expand Down
4 changes: 2 additions & 2 deletions accounts/tests_timezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
from django.urls import reverse

from accounts.tests_base import (
DATAMAILER_DISABLED_SETTINGS,
RELAY_DISABLED_SETTINGS,
AccountCourseTestCase,
)


@override_settings(**DATAMAILER_DISABLED_SETTINGS)
@override_settings(**RELAY_DISABLED_SETTINGS)
class AccountTimezonePreferenceTestCase(AccountCourseTestCase):
def post_timezone_preference(self, payload):
url = reverse("update_timezone_preference")
Expand Down
44 changes: 22 additions & 22 deletions api/openapi/data_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,18 +151,18 @@
)
COURSE_CERTIFICATES_OPERATION = operation(COURSE_CERTIFICATES_DATA)

DATAMAILER_EVENTS_SUCCESS_RESPONSE = schema_response(
RELAY_EVENTS_SUCCESS_RESPONSE = schema_response(
"Datamailer event accepted",
"DatamailerEventAccepted",
)
DATAMAILER_EVENTS_RESPONSES = {
"200": DATAMAILER_EVENTS_SUCCESS_RESPONSE,
RELAY_EVENTS_RESPONSES = {
"200": RELAY_EVENTS_SUCCESS_RESPONSE,
"400": INVALID_EVENT_PAYLOAD_RESPONSE,
"401": INVALID_WEBHOOK_TOKEN_RESPONSE,
"503": WEBHOOK_NOT_CONFIGURED_RESPONSE,
}
DATAMAILER_EVENTS_BODY = schema_request_body("DatamailerEvent")
DATAMAILER_EVENTS_DESCRIPTION = (
RELAY_EVENTS_BODY = schema_request_body("DatamailerEvent")
RELAY_EVENTS_DESCRIPTION = (
"Webhook used by Datamailer to report hard bounces, "
"complaints, subscription changes, skipped/failed sends, and "
"message lifecycle events back to CMP for support and audit "
Expand All @@ -171,25 +171,25 @@
"configured Datamailer webhook token in the Authorization "
"bearer token or X-Datamailer-Webhook-Token header."
)
DATAMAILER_EVENTS_DATA = OperationData(
RELAY_EVENTS_DATA = OperationData(
"api_datamailer_events",
["Datamailer"],
"Receive Datamailer contact event",
DATAMAILER_EVENTS_RESPONSES,
body=DATAMAILER_EVENTS_BODY,
RELAY_EVENTS_RESPONSES,
body=RELAY_EVENTS_BODY,
requires_auth=False,
description=DATAMAILER_EVENTS_DESCRIPTION,
description=RELAY_EVENTS_DESCRIPTION,
)
DATAMAILER_EVENTS_OPERATION = operation(DATAMAILER_EVENTS_DATA)
RELAY_EVENTS_OPERATION = operation(RELAY_EVENTS_DATA)

DATAMAILER_SEND_AUDITS_SUCCESS_RESPONSE = schema_response(
RELAY_SEND_AUDITS_SUCCESS_RESPONSE = schema_response(
"Datamailer send audits",
"DatamailerSendAudits",
)
DATAMAILER_SEND_AUDITS_RESPONSES = {
"200": DATAMAILER_SEND_AUDITS_SUCCESS_RESPONSE,
RELAY_SEND_AUDITS_RESPONSES = {
"200": RELAY_SEND_AUDITS_SUCCESS_RESPONSE,
}
DATAMAILER_SEND_AUDITS_PARAMETERS = [
RELAY_SEND_AUDITS_PARAMETERS = [
{
"name": "email",
"in": "query",
Expand All @@ -215,23 +215,23 @@
"schema": {"type": "integer", "default": 25, "maximum": 100},
},
]
DATAMAILER_SEND_AUDITS_DESCRIPTION = (
RELAY_SEND_AUDITS_DESCRIPTION = (
"Lists CMP's own Datamailer send-audit rows (one per send attempt "
"through the outbox -> dispatch -> /api/transactional/send pipeline). "
"Each row's response_payload carries the message summary and, when the "
"send ran with Datamailer's dry_run flag, the rendered subject/bodies. "
"Used by the e2e smoke suite to verify the rendered email over HTTP "
"without delivering anything. Ordered newest first."
)
DATAMAILER_SEND_AUDITS_DATA = OperationData(
RELAY_SEND_AUDITS_DATA = OperationData(
"api_datamailer_send_audits",
["Datamailer"],
"List Datamailer send audits",
DATAMAILER_SEND_AUDITS_RESPONSES,
parameters=DATAMAILER_SEND_AUDITS_PARAMETERS,
description=DATAMAILER_SEND_AUDITS_DESCRIPTION,
RELAY_SEND_AUDITS_RESPONSES,
parameters=RELAY_SEND_AUDITS_PARAMETERS,
description=RELAY_SEND_AUDITS_DESCRIPTION,
)
DATAMAILER_SEND_AUDITS_OPERATION = operation(DATAMAILER_SEND_AUDITS_DATA)
RELAY_SEND_AUDITS_OPERATION = operation(RELAY_SEND_AUDITS_DATA)

DATA_PATHS_BY_URL_NAME = {
"api_health": {
Expand All @@ -256,9 +256,9 @@
"post": COURSE_CERTIFICATES_OPERATION,
},
"api_datamailer_events": {
"post": DATAMAILER_EVENTS_OPERATION,
"post": RELAY_EVENTS_OPERATION,
},
"api_datamailer_send_audits": {
"get": DATAMAILER_SEND_AUDITS_OPERATION,
"get": RELAY_SEND_AUDITS_OPERATION,
},
}
4 changes: 2 additions & 2 deletions api/views/datamailer_webhook_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def bearer_token(request):


def authenticate_webhook(request):
expected = getattr(settings, "DATAMAILER_WEBHOOK_TOKEN", "")
expected = getattr(settings, "RELAY_WEBHOOK_TOKEN", "")
if not expected:
return False
token = bearer_token(request)
Expand Down Expand Up @@ -159,7 +159,7 @@ def load_webhook_fields(data):


def validate_webhook_configuration(data):
webhook_token = getattr(settings, "DATAMAILER_WEBHOOK_TOKEN", "")
webhook_token = getattr(settings, "RELAY_WEBHOOK_TOKEN", "")
if webhook_token:
return None
error = webhook_error(
Expand Down
10 changes: 5 additions & 5 deletions cadmin/tests/campaign_view_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from courses.models import Course, RegistrationCampaign, User


DATAMAILER_SETTINGS = {
"DATAMAILER_URL": "https://datamailer.example.com",
"DATAMAILER_API_KEY": "secret-token",
"DATAMAILER_CLIENT": "dtc-courses",
"DATAMAILER_AUDIENCE": "dtc-courses",
RELAY_SETTINGS = {
"RELAY_URL": "https://relay.example.com",
"RELAY_API_KEY": "secret-token",
"RELAY_CLIENT": "dtc-courses",
"RELAY_AUDIENCE": "dtc-courses",
}


Expand Down
12 changes: 6 additions & 6 deletions cadmin/tests/test_campaign_datamailer_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
from django.urls import reverse

from cadmin.tests.campaign_view_base import (
DATAMAILER_SETTINGS,
RELAY_SETTINGS,
CampaignCadminViewBase,
admin_credentials,
)


class CampaignDatamailerCadminViewTests(CampaignCadminViewBase):
@override_settings(
**DATAMAILER_SETTINGS,
**RELAY_SETTINGS,
PUBLIC_BASE_URL="https://courses.example.com",
)
@patch(
Expand All @@ -37,7 +37,7 @@ def test_campaign_edit_syncs_datamailer_campaign_draft(
self.assertRedirects(response, url)
self.assert_campaign_draft_upserted(upsert_campaign)

@override_settings(**DATAMAILER_SETTINGS)
@override_settings(**RELAY_SETTINGS)
@patch(
"course_management.datamailer.client_campaigns.DatamailerCampaignClient.preview_campaign"
)
Expand Down Expand Up @@ -71,7 +71,7 @@ def test_campaign_edit_previews_datamailer_campaign(
self.assertContains(response, "Preview subject")
self.assertContains(response, "Preview text")

@override_settings(**DATAMAILER_SETTINGS)
@override_settings(**RELAY_SETTINGS)
@patch(
"course_management.datamailer.client_campaigns.DatamailerCampaignClient.test_send_campaign"
)
Expand Down Expand Up @@ -102,7 +102,7 @@ def test_campaign_edit_sends_datamailer_campaign_test(
expected_recipients,
)

@override_settings(**DATAMAILER_SETTINGS)
@override_settings(**RELAY_SETTINGS)
@patch(
"course_management.datamailer.client_campaigns.DatamailerCampaignClient.queue_campaign"
)
Expand All @@ -129,7 +129,7 @@ def test_campaign_edit_queues_datamailer_campaign(
"cmp-registration-llm-zoomcamp"
)

@override_settings(**DATAMAILER_SETTINGS)
@override_settings(**RELAY_SETTINGS)
@patch(
"course_management.datamailer.client_campaigns.DatamailerCampaignClient.cancel_campaign"
)
Expand Down
Loading