diff --git a/.sampo/changesets/quiet-beacon-push.md b/.sampo/changesets/quiet-beacon-push.md new file mode 100644 index 000000000..313d4787a --- /dev/null +++ b/.sampo/changesets/quiet-beacon-push.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: minor +--- + +Add `posthog.push.send()`, `AsyncPosthog.push.send()` and the standalone `Push` and `AsyncPush` classes to send transactional push notifications from a PostHog push template to every device of one person. Sends use the client's `secret_key` (a project secret API key with the `transactional_push:write` scope), `app_host`, and email request timeout and retries. diff --git a/posthog/async_client.py b/posthog/async_client.py index 7e5c3b3ff..c2e82c213 100644 --- a/posthog/async_client.py +++ b/posthog/async_client.py @@ -69,6 +69,7 @@ ) from .consumer import MAX_MSG_SIZE from .emails import AsyncEmails, _client_emails, _validate_options +from .push import AsyncPush from .feature_flag_evaluations import ( FeatureFlagEvaluations, _EvaluatedFlagRecord, @@ -306,6 +307,27 @@ def emails(self) -> AsyncEmails: ) return self._emails + @property + def push(self) -> AsyncPush: + """ + Send transactional push notifications from PostHog push templates. + + Needs ``secret_key``, a project secret API key with the ``transactional_push:write`` + scope. Sends go where ``emails`` sends go and use the same timeout and + retries. A disabled or shut-down client sends nothing. See ``posthog.push`` + for setup. + + Examples: + ```python + await client.push.send( + template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", + distinct_id="user_123", + template_data={"order_id": "1042"}, + ) + ``` + """ + return AsyncPush._over(self.emails) + def _admit_email_send(self) -> Optional[Callable[[], None]]: # Shutdown waits for in-flight operations before it closes the email transport. if not self._accepting or self._closed: diff --git a/posthog/client.py b/posthog/client.py index 35380d979..f81f6b9f3 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -79,6 +79,7 @@ tag as _context_tag, ) from posthog.emails import Emails, _client_emails, _no_op, _validate_options +from posthog.push import Push from posthog.exception_capture import ExceptionCapture from posthog._logging import _configure_posthog_logging from posthog.exception_utils import ( @@ -1726,6 +1727,27 @@ def emails(self) -> Emails: ) return self._emails + @property + def push(self) -> Push: + """ + Send transactional push notifications from PostHog push templates. + + Needs ``secret_key``, a project secret API key with the ``transactional_push:write`` + scope. Sends go where ``emails`` sends go and use the same timeout and + retries. A disabled or shut-down client sends nothing. See ``posthog.push`` + for setup. + + Examples: + ```python + client.push.send( + template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", + distinct_id="user_123", + template_data={"order_id": "1042"}, + ) + ``` + """ + return Push._over(self.emails) + @property def queue(self) -> Queue: """The analytics lane's queue (kept for backwards compatibility).""" diff --git a/posthog/emails.py b/posthog/emails.py index 42e70b971..b8a324fb7 100644 --- a/posthog/emails.py +++ b/posthog/emails.py @@ -920,6 +920,16 @@ def _deliver(self, prepared: _PreparedSend, secret_key: str) -> SendEmailResult: prepared.max_retries, ) + def _queue( + self, prepared: _PreparedSend, not_done: str, key_needed: str + ) -> SendEmailResult: + # posthog.push sends through this, so a push gets the same host, key, timeout and retries. + finish = self._start(not_done) + try: + return self._deliver(prepared, self._secret_key(key_needed)) + finally: + finish() + def get(self, message_id: Union[str, UUID]) -> EmailStatus: """Read what happened to an email that :meth:`send` queued. @@ -1104,6 +1114,15 @@ async def _deliver( prepared.max_retries, ) + async def _queue( + self, prepared: _PreparedSend, not_done: str, key_needed: str + ) -> SendEmailResult: + finish = self._start(not_done) + try: + return await self._deliver(prepared, self._secret_key(key_needed)) + finally: + finish() + async def get(self, message_id: Union[str, UUID]) -> EmailStatus: """Read what happened to an email that :meth:`send` queued. @@ -1234,7 +1253,7 @@ def _client_emails( if not is_cloud: # A custom host is often an ingestion proxy, which must not receive the secret key. emails._host_error = ( - "The client's host is not PostHog Cloud, so the client can't tell where to send email. " + "The client's host is not PostHog Cloud, so the client can't tell where to send requests. " "Set app_host to your PostHog app host on the client." ) emails._parent_disabled = is_disabled diff --git a/posthog/push.py b/posthog/push.py new file mode 100644 index 000000000..e2b1438cd --- /dev/null +++ b/posthog/push.py @@ -0,0 +1,360 @@ +"""Transactional push notifications, sent from a push template in PostHog. + +Setup: + +1. In PostHog, open the push notification under Workflows, make it sendable + from the API, and copy its ID. ``template_id`` is that UUID. +2. Create a project secret API key (``phs_...``) with the + ``transactional_push:write`` scope in your project settings. +3. Send from the client you already use, or from :class:`Push` in a process + that only sends notifications: + +```python +from posthog import Posthog + +posthog = Posthog("phc_...", secret_key="phs_...", host="https://eu.i.posthog.com") +result = posthog.push.send( + template_id="