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 }.

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 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