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 }.
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.
NSS Example Configs
More NSS configs live in the notification repo, including the sample config.
Example blast-oriented config—enable the channels you need (in-app, push, email):
notification_name: tos_update
notification_version: v1
skip_preference_check: true
channels:
in-app:
enabled: true
push:
enabled: true
sending_platform: firebase
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