-
Notifications
You must be signed in to change notification settings - Fork 10
ASA Go: Send FCM push notifications when fire behaviour advisories are issued #5246
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
Merged
Merged
Changes from all commits
Commits
Show all changes
83 commits
Select commit
Hold shift + click to select a range
7f05edf
sending hooked up
conbrad 916a864
cleanup
conbrad 344e413
Merge branch 'main' into task/send-fcm-notifications
conbrad 0d6bb73
remove TODO
conbrad 01326fd
FCM creds
conbrad abbf802
hook up to process stats
conbrad 7f53e74
flush on upsert, hook up app
conbrad cbf12d9
fixes and tests
conbrad 6f94abb
fixes
conbrad 4f70114
fix
conbrad b9c5be7
tag groups
conbrad 926dc60
cleanup
conbrad 4549426
add tests
conbrad 7fd7ed2
tests for process_stats
conbrad 5beda5c
cleanup tests
conbrad d2b7194
add test
conbrad 20dd931
set threshold
conbrad 2a3bcb4
bug fix, cleanup
conbrad 4693591
bug fix, cleanup
conbrad 7092310
Simplify cleanup
conbrad 1b527e4
comment
conbrad 7196112
manage blocking call
conbrad 28c6b7a
forecasts for today send notifications
conbrad f9f1e7a
Set lifespan for notifications to 2 days
conbrad 962ed4e
Batch messaging
conbrad f724bcb
remove await
conbrad de1fd00
capture previous subs to replace if server update fails
conbrad 99b98d7
Hook up to map context menu
conbrad 055b43c
fix tests
conbrad 7e88251
deviceID error handling
conbrad 65a3e97
state machine for notifications
conbrad 66e71b4
state machine cleanup and retry logic
conbrad b40dc98
reuse code
conbrad f62f4ef
initsubsonce
conbrad 6b3e975
device and register error handling
conbrad 4793133
type fixes
conbrad 5abf8ee
simplify init subs
conbrad 848a848
update diagram
conbrad f751485
Fix notification tests
conbrad 0ce34db
reuse error message
conbrad 2a9d3e9
fix tests
conbrad 76745d0
Merge branch 'main' into task/send-fcm-notifications
conbrad 5b404e1
fix tests and imports
conbrad d7be929
add error tests
conbrad 40df06d
remove acts
conbrad fe2eb8c
fix import
conbrad 3d3f4b3
error if no placename
conbrad 639c48d
update diagram
conbrad f94fa80
Fix notification title
conbrad 6cab844
settingsSlice tests
conbrad 10917ef
registrationFailed test
conbrad d682f2f
Trigger Build
conbrad 52c457e
Merge branch 'main' into task/send-fcm-notifications
conbrad e410bc2
more logging
conbrad f903c48
undo batching
conbrad ffed8cd
Revert "undo batching"
conbrad d591d03
add more logging
conbrad 8028e5f
pydantic model
conbrad ad23ba2
not toggle error, snackbar at top with close button
conbrad 2dd5621
remove disabled reason from context menu
conbrad 4bbc3c5
Add retry registration logic to context menu open
conbrad dae425c
fix error message
conbrad 020a42c
Merge branch 'main' into task/send-fcm-notifications
conbrad d096cb4
250ms base delay
conbrad 8c6e2ba
Merge branch 'main' into task/send-fcm-notifications
conbrad c364e54
notification error snackbar
conbrad c2855cb
Merge branch 'main' into task/send-fcm-notifications
conbrad 7cabde8
refactor: move retry registration logic into dedicated useRetryRegist…
conbrad 0a41807
move registration into hook
conbrad 7c01f34
add styling to notification error
conbrad 813126f
Merge branch 'main' into task/send-fcm-notifications
conbrad 40c5336
Merge branch 'main' into task/send-fcm-notifications
conbrad efca4c4
max limit of retries
conbrad a2d40c9
type annotations
conbrad ceda0b3
sub utils
conbrad 9b8ef97
getToken retries
conbrad b401ed8
retryRegistration resets attempts
conbrad 64a4871
adjust tests
conbrad a7f087a
Merge branch 'main' into task/send-fcm-notifications
conbrad 23c4985
Merge branch 'main' into task/send-fcm-notifications
conbrad 5835aa9
Merge branch 'main' into task/send-fcm-notifications
conbrad d0e76f2
afternoon notification filter
conbrad 5e20ae0
Merge branch 'main' into task/send-fcm-notifications
conbrad File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
19 changes: 19 additions & 0 deletions
19
.vscode/bruno-collections/Predictive Services API/FCM/Get Notification Settings.bru
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| meta { | ||
| name: Get Notification Settings | ||
| type: http | ||
| seq: 1 | ||
| } | ||
|
|
||
| get { | ||
| url: {{API_BASE_URL}}/device/notification-settings?device_id=9242842270074025 | ||
| body: none | ||
| auth: bearer | ||
| } | ||
|
|
||
| params:query { | ||
| device_id: 9242842270074025 | ||
| } | ||
|
|
||
| auth:bearer { | ||
| token: {{AUTH_TOKEN}} | ||
| } |
22 changes: 22 additions & 0 deletions
22
.vscode/bruno-collections/Predictive Services API/FCM/Update Notification Settings.bru
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| meta { | ||
| name: Update Notification Settings | ||
| type: http | ||
| seq: 2 | ||
| } | ||
|
|
||
| post { | ||
| url: {{API_BASE_URL}}/device/notification-settings | ||
| body: json | ||
| auth: bearer | ||
| } | ||
|
|
||
| auth:bearer { | ||
| token: {{AUTH_TOKEN}} | ||
| } | ||
|
|
||
| body:json { | ||
| { | ||
| "device_id": "9242842270074025", | ||
| "fire_zone_source_ids": ["24"] | ||
| } | ||
| } |
6 changes: 6 additions & 0 deletions
6
.vscode/bruno-collections/Predictive Services API/environments/dev.bru
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| vars { | ||
| API_BASE_URL: https://wps-pr-5246-e1e498-dev.apps.silver.devops.gov.bc.ca/api | ||
| } | ||
| vars:secret [ | ||
| AUTH_TOKEN | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import asyncio | ||
| import logging | ||
| from datetime import date, datetime, timedelta, timezone | ||
|
|
||
| from firebase_admin import exceptions as firebase_exceptions | ||
| from firebase_admin import messaging | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
| from wps_shared import config | ||
| from wps_shared.db.crud.auto_spatial_advisory import ZoneAdvisoryStatus, get_zones_with_advisories | ||
| from wps_shared.db.crud.fcm import get_device_tokens_for_zone, update_device_tokens_are_active | ||
| from wps_shared.db.models.auto_spatial_advisory import RunTypeEnum | ||
| from wps_shared.utils.time import get_vancouver_now | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| FCM_BATCH_SIZE = 500 | ||
|
|
||
|
|
||
| def build_notification_content(zone_with_advisory: ZoneAdvisoryStatus, for_date: date): | ||
| return f"{for_date.strftime('%a, %B %-d')} - An advisory has been issued for {zone_with_advisory.placename_label}" | ||
|
|
||
|
|
||
| def build_notification_title(zone_with_advisory: ZoneAdvisoryStatus): | ||
| zone = zone_with_advisory.placename_label.split("-")[0] | ||
| return f"Fire Behaviour Advisory, {zone}" | ||
|
|
||
|
|
||
| def build_fcm_message( | ||
| for_date: date, zone_with_advisory: ZoneAdvisoryStatus, device_tokens: list[str] | ||
| ): | ||
| title = build_notification_title(zone_with_advisory) | ||
| content = build_notification_content(zone_with_advisory, for_date) | ||
| tag = f"advisory-{zone_with_advisory.source_identifier}" | ||
brettedw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ttl = timedelta(days=2) | ||
| apns_expiration = str(int((datetime.now(timezone.utc) + ttl).timestamp())) | ||
| message = messaging.MulticastMessage( | ||
| notification=messaging.Notification(title=title, body=content), | ||
| android=messaging.AndroidConfig( | ||
| ttl=ttl, notification=messaging.AndroidNotification(tag=tag) | ||
| ), | ||
| apns=messaging.APNSConfig( | ||
| headers={"apns-expiration": apns_expiration}, | ||
| payload=messaging.APNSPayload(aps=messaging.Aps(thread_id=tag)), | ||
| ), | ||
| tokens=device_tokens, | ||
| ) | ||
|
|
||
| return message | ||
|
|
||
|
|
||
| async def trigger_notifications( | ||
| session: AsyncSession, run_type: RunTypeEnum, run_datetime: datetime, for_date: date | ||
| ) -> None: | ||
| if run_type == RunTypeEnum.actual: | ||
| return | ||
|
|
||
| vancouver_now = get_vancouver_now() | ||
|
|
||
| if for_date != vancouver_now.date(): | ||
| logger.info("Skipping FCM notifications: for_date=%s is not today", for_date) | ||
| return | ||
|
|
||
| if config.get("ENVIRONMENT") == "production": | ||
| if vancouver_now.hour >= 12: | ||
|
Check warning on line 64 in backend/packages/wps-api/src/app/fcm/notifications.py
|
||
| logger.info( | ||
| "Skipping FCM notifications: current Vancouver time hour=%d is at or after noon", | ||
| vancouver_now.hour, | ||
| ) | ||
| return | ||
|
|
||
| logger.info("Checking for warnings/advisories to send FCM notifications for") | ||
| zones_with_advisories = await get_zones_with_advisories( | ||
| session, run_type, run_datetime, for_date | ||
| ) | ||
| logger.info( | ||
| f"{len(zones_with_advisories)} have warnings/advisories, checking for devices to notify" | ||
| ) | ||
| for zone_with_advisory in zones_with_advisories: | ||
| if not zone_with_advisory.placename_label: | ||
| logger.error( | ||
| "Skipping FCM notification: missing placename_label for zone source_identifier=%s", | ||
| zone_with_advisory.source_identifier, | ||
| ) | ||
| continue | ||
| device_tokens = await get_device_tokens_for_zone( | ||
| session, zone_with_advisory.source_identifier | ||
| ) | ||
| if len(device_tokens) == 0: | ||
| logger.info(f"No devices subscribed to {zone_with_advisory.placename_label}") | ||
| continue | ||
| logger.info( | ||
| f"{len(device_tokens)} are subscribed to {zone_with_advisory.placename_label} about to notify" | ||
| ) | ||
| for i in range(0, len(device_tokens), FCM_BATCH_SIZE): | ||
| batch = device_tokens[i : i + FCM_BATCH_SIZE] | ||
| message = build_fcm_message(for_date, zone_with_advisory, batch) | ||
| try: | ||
| logger.info(f"Notifiying {len(batch)} devices") | ||
| # messaging.send_each_for_multicast is a synchronous blocking call | ||
| response = await asyncio.to_thread(messaging.send_each_for_multicast, message) | ||
| except firebase_exceptions.FirebaseError: | ||
| logger.exception( | ||
| "FCM send failed for zone=%s date=%s token_count=%d", | ||
| zone_with_advisory.placename_label, | ||
| for_date, | ||
| len(batch), | ||
| ) | ||
| continue | ||
| await handle_fcm_response( | ||
| session, for_date, zone_with_advisory.placename_label, batch, response | ||
| ) | ||
|
|
||
|
|
||
| async def handle_fcm_response( | ||
| session: AsyncSession, | ||
| for_date: date, | ||
| placename_label: str, | ||
| device_tokens: list[str], | ||
| response: messaging.BatchResponse, | ||
| ): | ||
| logger.info( | ||
| f"Received FCM response with successful notifications sent: {response.success_count}" | ||
| ) | ||
| # Only deactivate permanently invalid tokens (UnregisteredError — token is no longer registered). | ||
| # Transient failures (quota, server errors) are not deactivated; the token remains valid. | ||
| # Deactivated tokens are re-activated when the app re-registers on open. | ||
| permanently_failed = [ | ||
| device_tokens[idx] | ||
| for idx, resp in enumerate(response.responses) | ||
| if not resp.success and isinstance(resp.exception, messaging.UnregisteredError) | ||
| ] | ||
| transient_failed_count = response.failure_count - len(permanently_failed) | ||
|
|
||
| if response.failure_count > 0: | ||
| logger.warning( | ||
| "FCM send for zone=%s date=%s: %d permanent failures, %d transient failures out of %d tokens", | ||
| placename_label, | ||
| for_date, | ||
| len(permanently_failed), | ||
| transient_failed_count, | ||
| len(device_tokens), | ||
| ) | ||
|
|
||
| if permanently_failed: | ||
| await update_device_tokens_are_active(session, permanently_failed, False) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.