Notifications Send Blaster
Overview
The Notifications Send Blaster (NSB) is a Handshake rake task that sends any notification registered with Notifications Sending Service (NSS) to a chosen set of users—typically a one-time blast to many student users.
This page is the single source of truth for NSB; the Confluence write-up has been consolidated here. The rake task and worker were introduced in Handshake PR #68092.
We have used this task to send notifications to ~55M users over the course of a few hours, but ideally user groups would be maximum 3M ids. If you need to send to a larger subset of users, ask the notifications team.
Example use cases
-
Alert users to Handshake terms of service change, or
-
Showcase feed posts, or
-
Advertise Handshake events/promotions, or
-
Display quick-start tips and tricks
How to use
Prerequisites
- User list — choose one mode:
- Direct user ID list: Comma-separated
USER_IDS. Best for testing or small lists. - GCS bucket CSV: For large lists (tested with 690k successfully). You need access to the notifications scratch bucket: add your user in the notifications terraform repo (ask the notifications team for the relevant PR or repo link). After merging, use go/spacelift and apply the terraform changes to all environments. Upload a CSV or export from BigQuery. The CSV must have a header row with a column named
id. BigQuery may output multiple files (e.g.0000.csv,0001.csv).
- Direct user ID list: Comma-separated
- NSS config — add a config in the notifications repo; see NSS Example Configs.
- Templates — add template(s) for enabled channels (in-app, push, email).
- Example PRs: NSS sample config / blast setup, in-app templates.
Sending a notification blast
- Prepare your user list (example user queries or export from BigQuery).
- Add NSS config and templates (see Prerequisites).
- Generate your notification locally; see Useful Commands.
- Verify the notification (in-app on frontend, push on device, etc.).
- Run the NSB task. For dry run vs send, see CONFIRM and dry run.
Run the task
Direct user ID list (small list / testing)
Set USER_IDS to a comma-separated list of user ids.
Example (dry run):
CONFIRM=false USER_IDS=1,2,3 bundle exec rails "notifications:send_notifications['notification_name=example_notification¬ification_version=1']"
Example (actually send):
CONFIRM=true USER_IDS=12345,67890 bundle exec rails "notifications:send_notifications['notification_name=feed_content_post_recommendation¬ification_version=1&attributes[content_post_id]=1940&attributes[content_text]=👀 Looking to elevate your interview game? Do not sleep on these recruiter tips to take your interviews to the next level 😏']"
feed_content_post_recommendation notes
feed_content_post_recommendation works in one locale per blast: if content_text is US English, it only sends to users in US English.
You can use emojis or curly/smart apostrophes in content_text; other quote characters can break the inline deployer command.
CONFIRM=false USER_IDS=1,2,3 bundle exec rails "notifications:send_notifications['notification_name=feed_content_post_recommendation¬ification_version=1&attributes[content_post_id]=1&attributes[content_text]=You can use emojis or single quotations, as long as it’s this type 👀 other marks will break the inline deployer command!']"
GCS bucket CSV (large list)
Use the correct Deployer environment and matching -notifications-scratch bucket, or the task will fail.
Set:
USE_BUCKET_CSV=trueGOOGLE_CLOUD_STORAGE_BUCKET— e.g.handshake-production-notifications-scratch(production)GOOGLE_CLOUD_STORAGE_PATH— object path within the bucket, e.g. top level2024_03_03_q1_swe_segment_000000000000.csvor nestedmega_blast/v1/segment00000001.csv
Example (dry run):
CONFIRM=false USE_BUCKET_CSV=true GOOGLE_CLOUD_STORAGE_BUCKET=handshake-production-notifications-scratch GOOGLE_CLOUD_STORAGE_PATH=000000000000.csv bundle exec rails "notifications:send_notifications['notification_name=example_notification¬ification_version=1']"
Example (actually send):
CONFIRM=true USE_BUCKET_CSV=true GOOGLE_CLOUD_STORAGE_BUCKET=handshake-production-notifications-scratch GOOGLE_CLOUD_STORAGE_PATH=2024_03_03_q1_swe_segment_000000000000.csv bundle exec rails "notifications:send_notifications['notification_name=feed_content_post_recommendation¬ification_version=1&attributes[content_post_id]=1940&attributes[content_text]=👀 Looking to elevate your interview game? Do not sleep on these recruiter tips to take your interviews to the next level 😏']"
Expected output
Running the task does not guarantee the blast succeeded end-to-end.
CONFIRM=false USER_IDS=1,2,3 bundle exec rails "notifications:send_notifications[notification_name=example_notification¬ification_version=1"
[IdentityCache] Missing CAS support in cache backend ActiveSupport::Cache::RedisCacheStore which is needed for cache consistency
I, [2024-03-28T23:04:49.635883 #113869] INFO -- : LaunchDarkly disabled!
I, [2024-03-28T23:04:49.636034 #113869] INFO -- : LaunchDarkly disabled!
D, [2024-03-28T23:04:49.658803 #113869] DEBUG -- : Attempting to use Pubsub Emulator at localhost:8087...
D, [2024-03-28T23:04:49.662827 #113869] DEBUG -- : Pubsub connected in development
I, [2024-04-01T19:06:59.539075 #50285] INFO -- : {"notification_name"=>"example_notification", "notification_version"=>"1"}
I, [2024-04-01T19:06:59.539203 #50285] INFO -- : notification_name: example_notification
I, [2024-04-01T19:06:59.539259 #50285] INFO -- : notification_version: 1
I, [2024-04-01T19:06:59.539422 #50285] INFO -- : user_ids: 3
I, [2024-04-01T19:06:59.539501 #50285] INFO -- : Sending notifications to: 3 users
I, [2024-04-01T19:06:59.543977 #50285] INFO -- : Batch 1 sent
I, [2024-04-01T19:06:59.544345 #50285] INFO -- : Send Notifications task finished (workers running asynchronously)
Verify blast records in NDS
Check that rows were created in NDS.
For in-app:
SELECT *
FROM `notifications_data_service.in_app_notifications` i
JOIN `notifications_data_service.notifications` n
ON n.notification_id = i.notification_id
WHERE n.notification_name = 'uk_live_rent_free_campaign'
For push:
SELECT *
FROM `notifications_data_service.push_notifications` p
JOIN `notifications_data_service.notifications` n
ON n.notification_id = p.notification_id
WHERE n.notification_name = 'uk_live_rent_free_campaign'
Row counts may be lower than the number of ids in the blast: after send, user preferences are applied and requests are rejected when a preference is disabled (many users disable marketing preferences).
Reference
Task arguments
The rake task accepts the following (passed as query args in the task string):
- notification_name — NSS notification name
- notification_version — NSS notification version
- attributes — Hash of base attributes applied with each notification (e.g.
job_id,content_post_id,content_text). Pass as a query string; see this Stack Overflow note on format. Example:attributes[job_id]=123&attributes[posting_id]=456producesattributes: { job_id: 123, posting_id: 456 }. - channels_filter — Optional list of channels to scope the blast to a subset of channels enabled in the config. Values:
email,push,in_app. Maps onto thechannels_filterfield onSendNotificationBatch. Omit to send to all enabled channels. See Scoping channels per blast.
CONFIRM and dry run
- CONFIRM=true — Required to actually send notifications.
- CONFIRM=false — Prints what would be sent and stops before sending. Use for testing.
Scoping channels per blast
If you want one blast to fire only push and another only in-app from the same NSS config, prefer enabling all the channels you might need on a single config and using channels_filter per send instead of forking a separate config version for every channel mix.
The filter accepts any subset of the channels enabled on the config. Channels listed in the filter must be enabled in the config — passing a disabled channel returns a 400.
Example — blast push only from a config that has all three channels enabled:
CONFIRM=true USER_IDS=12345,67890 bundle exec rails "notifications:send_notifications['notification_name=tos_update¬ification_version=1&channels_filter[]=push']"
Example — blast email and in-app:
CONFIRM=true USER_IDS=12345,67890 bundle exec rails "notifications:send_notifications['notification_name=tos_update¬ification_version=1&channels_filter[]=email&channels_filter[]=in_app']"
Omitting the field sends to all enabled channels on the config (subject to user preferences).
NSS Example Configs
More NSS configs live in the notification repo, including the sample config.
Example blast-oriented config — enable every channel you might want to blast on, then use channels_filter at send time to scope each blast (see Scoping channels per blast):
notification_name: tos_update
notification_version: v1
skip_preference_check: true
channels:
in-app:
enabled: true
push:
enabled: true
sending_platform: firebase
email:
enabled: true
sending_platform: mailgun
sending_domain_name: notification_domain
mailer:
name: TosUpdateMailer
method: email
If your blast has a notification preference associated with it, you can check it automatically by setting notification_preference_name to that preference.
Rake task gotchas
When running this task in the deployer:
- You must have double quotations (
") around the task (everything afterbundle exec rails). - You must have single quotations (
') around the string options. - You cannot have any spaces after commas, or outside of strings.
- You cannot use pipes in the arguments—the task will break with "Don't know how to build task".
- You cannot use a single quote (
') inside the attributes argument at this time. (TODO: provide another way to load this info, e.g. a JSON file.)
For more rake context, see Other Rake info.
Useful Commands
Commands to generate and preview notifications in the Handshake app before blasting:
Example User Queries
- Query for US users active in last 90 days:
SELECT Users.id
FROM `handshake-production.handshake.users` as Users
WHERE Users.user_type = 'Students'
AND Users.status = 'active'
AND Users.last_logged_in between (TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)) and CURRENT_TIMESTAMP()
ORDER BY id
- Query for all UK Students:
SELECT Users.id
FROM `handshake-eu-production.handshake.users` as Users
JOIN `handshake-eu-production.handshake.schools` as Schools
ON Users.institution_id = Schools.id
WHERE user_type = 'Students'
AND Schools.region = 'UK'
ORDER BY id
Example Notification Queries
- Query for push campaign success
- By "success", we mean how many push notifications we requested were actually created, not whether they were opened.
- Replace
${notification_name}with the template name.
SELECT countif(p.failed_at is not null) fail_count, countif(p.sent_at is not null) send_count, countif(p.sent_at is not null)/count(*)
FROM `notifications_data_service.push_notifications` p
JOIN `notifications_data_service.notifications` n
ON n.notification_id = p.notification_id
WHERE n.notification_name = '${notification_name}'
- Query for push campaign open rates
- Replace
tagwith the template name. - Uses lifecycle_communication_messages.
- Replace
SELECT COUNT(*)
FROM `handshake-eu-production.handshake_derived.lifecycle_communication_messages`
WHERE channel = 'push'
AND tag = 'uk_live_rent_free_campaign'
AND first_clicked_at is not null
- CAUTION: The following query should only be used while testing. It reads Segment directly (faster than LCM). LCM runs incrementally (events for messages sent in the past ~30 days, every two hours) and full-refresh weekly. For an older notification you may see stale stats before the weekly run.
- Replace
uk_live_rent_free_campaignandtime_stampas needed.
SELECT properties, current_user_id
FROM `segment.all_segment_events`
WHERE event_name = 'push_notification_opened'
AND time_stamp >= '2024-10-17'
AND (JSON_VALUE(properties, '$.type') = 'uk_live_rent_free_campaign' OR JSON_VALUE(properties, '$.category') = 'uk_live_rent_free_campaign')
- Query for push campaign open rates (iOS only)
- For Android, replace
categorywithtype. - Replace template name and
time_stampas needed.
- For Android, replace
SELECT properties, current_user_id
FROM `segment.all_segment_events`
WHERE event_name = 'push_notification_opened'
AND time_stamp >= '2024-10-17'
AND JSON_VALUE(properties, '$.category') = 'uk_live_rent_free_campaign'
Export ids from BigQuery
Export from BigQuery with EXPORT DATA OPTIONS:
Generic export:
EXPORT DATA
OPTIONS ( uri = 'gs://handshake-production-notifications-scratch/*.csv',
format = 'CSV',
OVERWRITE = TRUE,
header = TRUE) AS (
# YOUR_QUERY_HERE
)
- For EU, use
handshake-production-eu-notifications-scratchinstead ofhandshake-production-notifications-scratch.
Example: export all Q1 SWE segment users to GCS (BQ may emit multiple files, e.g. 0000.csv, 0001.csv):
EXPORT DATA
OPTIONS ( uri = 'gs://handshake-production-notifications-scratch/*.csv',
format = 'CSV',
OVERWRITE = TRUE,
header = TRUE) AS (
SELECT
DISTINCT(user_id) AS id
FROM
`handshake-production.marketplace.cnc_target_segments`
WHERE
is_q1_swe_segment = TRUE
ORDER BY
user_id
)
Create a notification (triggers all enabled channels)
curl -s -v \
-H "Authorization: Bearer apitoken" \
-H "Content-Type: application/json" \
-X POST \
-d '{"notification_name":"example_notification_name", "notification_version":"1", "base_attributes": {}, "items": [{"user_id":125}, {"user_id": 124}]}' \
http://localhost:5550/v1/notifications/batch | jq
Verify users got in-app notification
curl -s -v \
-H "Authorization: Bearer apitoken" \
-H "Content-Type: application/json" \
http://localhost:5551/v1/users/125/notifications/in-app | jq
curl -s -v \
-H "Authorization: Bearer apitoken" \
-H "Content-Type: application/json" \
http://localhost:5551/v1/users/124/notifications/in-app | jq