Skip to main content

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​

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).
  • 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​

  1. Prepare your user list (example user queries or export from BigQuery).
  2. Add NSS config and templates (see Prerequisites).
  3. Generate your notification locally; see Useful Commands.
  4. Verify the notification (in-app on frontend, push on device, etc.).
  5. 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&notification_version=1']"

Example (actually send):

CONFIRM=true USER_IDS=12345,67890 bundle exec rails "notifications:send_notifications['notification_name=feed_content_post_recommendation&notification_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&notification_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=true
  • GOOGLE_CLOUD_STORAGE_BUCKET — e.g. handshake-production-notifications-scratch (production)
  • GOOGLE_CLOUD_STORAGE_PATH — object path within the bucket, e.g. top level 2024_03_03_q1_swe_segment_000000000000.csv or nested mega_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&notification_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&notification_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&notification_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]=456 produces attributes: { 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 the channels_filter field on SendNotificationBatch. 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&notification_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&notification_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 after bundle 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}'
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_campaign and time_stamp as 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 category with type.
    • Replace template name and time_stamp as 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, '$.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-scratch instead of handshake-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