Skip to content

fix: recover notification CTR with celebratory copy and dynamic push headings#3682

Merged
nimrodkra merged 3 commits intomainfrom
nimrod/notification-ctr-recovery
Mar 10, 2026
Merged

fix: recover notification CTR with celebratory copy and dynamic push headings#3682
nimrodkra merged 3 commits intomainfrom
nimrod/notification-ctr-recovery

Conversation

@nimrodkra
Copy link
Member

Summary

  • Restore celebratory openers and CTAs for notification types that declined after refactor: overhaul notification copy across 64 types #3657
  • Rewrite upvote milestones with geeky tiered copy (dev/gaming references)
  • Add dynamic push headings that include squad names and commenter names
  • Update static push headings for achievement and milestone types

Context

Post-#3657 data showed CTR declines across 7 notification types. Two root causes: removing celebratory tone from achievement/milestone types turned them into system messages, and replacing the curiosity-inducing "New update" heading with bland specific headings removed the reason to tap.

Changes target only the declining types. All improvements from #3657 are preserved (source_post_approved +106.5%, post_bookmark_reminder +23.4%, comment_mention +17.6%).

Risk

String-literal changes plus a small function addition in onesignal.ts for dynamic push headings. No logic, data model, or migration changes. Dynamic headings fall back to existing static strings if title parsing fails.

Test plan

  • Build passes
  • Lint passes
  • Updated test assertions for squad_member_joined title and upvote milestone email copy

Made with Cursor

…headings

Post-#3657 data showed CTR declines across 7 notification types. Root
causes: lost celebratory tone in achievement/milestone types, and bland
push headings replacing the curiosity-inducing "New update" default.

Title changes:
- user_given_top_reader: restore "Great news!" opener (-84.6% without it)
- achievement_unlocked: add "You earned" for personal warmth
- squad_member_joined: add "Say hi!" CTA
- Upvote milestones: geeky tiered copy (dev/gaming references) replacing
  dashboard-stat "number. remark" format

Push heading changes:
- Dynamic headings for squad_post_added ("New post in {squad}"),
  squad_new_comment and article_new_comment ("{name} commented")
- achievement_unlocked: "Achievement unlocked" -> "Level up!"
- upvote milestones: "Milestone reached" -> "New milestone"

Preserves all improvements from #3657 (source_post_approved +106.5%,
post_bookmark_reminder +23.4%, comment_mention +17.6%).

Made-with: Cursor
@pulumi
Copy link

pulumi bot commented Mar 10, 2026

🍹 The Update (preview) for dailydotdev/api/prod (at db495f5) was successful.

✨ Neo Explanation

This is a standard application version rollout, updating all services and scheduled jobs to a new build, with accompanying database and ClickHouse schema migration jobs being run for the new version.

Root Cause Analysis

A new version of the application has been built and is being deployed. Every Kubernetes resource is being updated to point to the new container image, replacing the previous build across all services.

Dependency Chain

The new application image cascades across the entire platform:

  • 7 Deployments (main API, background jobs, WebSocket, worker, temporal, private, personalized digest) are rolling out the new image version
  • 34 CronJobs are updated to use the new image for their next scheduled runs
  • 2 one-time migration Jobs (database and ClickHouse) tied to the previous build are being deleted, and 2 new migration jobs for the new build are being created — these run the TypeORM and ClickHouse schema migrations as part of the deployment

Risk analysis

No stateful resources (databases, storage) are being replaced or deleted. The migration jobs are the most operationally significant part of this deployment — they run schema changes against the database and ClickHouse before (or alongside) the new application version rolling out. Deployments use rolling updates by default in Kubernetes, so no downtime is expected for live services.

Resource Changes

    Name                                                       Type                           Operation
+   vpc-native-api-clickhouse-migration-a0865745               kubernetes:batch/v1:Job        create
~   vpc-native-update-source-public-threshold-cron             kubernetes:batch/v1:CronJob    update
~   vpc-native-post-analytics-clickhouse-cron                  kubernetes:batch/v1:CronJob    update
~   vpc-native-update-tags-str-cron                            kubernetes:batch/v1:CronJob    update
~   vpc-native-generate-search-invites-cron                    kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-zombie-images-cron                        kubernetes:batch/v1:CronJob    update
~   vpc-native-update-tag-recommendations-cron                 kubernetes:batch/v1:CronJob    update
~   vpc-native-worker-job-deployment                           kubernetes:apps/v1:Deployment  update
~   vpc-native-check-analytics-report-cron                     kubernetes:batch/v1:CronJob    update
~   vpc-native-validate-active-users-cron                      kubernetes:batch/v1:CronJob    update
~   vpc-native-sync-subscription-with-cio-cron                 kubernetes:batch/v1:CronJob    update
~   vpc-native-calculate-top-readers-cron                      kubernetes:batch/v1:CronJob    update
~   vpc-native-user-profile-analytics-history-clickhouse-cron  kubernetes:batch/v1:CronJob    update
~   vpc-native-expire-super-agent-trial-cron                   kubernetes:batch/v1:CronJob    update
~   vpc-native-update-highlighted-views-cron                   kubernetes:batch/v1:CronJob    update
~   vpc-native-update-current-streak-cron                      kubernetes:batch/v1:CronJob    update
~   vpc-native-agents-digest-cron                              kubernetes:batch/v1:CronJob    update
~   vpc-native-generic-referral-reminder-cron                  kubernetes:batch/v1:CronJob    update
~   vpc-native-squad-posts-analytics-refresh-cron              kubernetes:batch/v1:CronJob    update
~   vpc-native-private-deployment                              kubernetes:apps/v1:Deployment  update
~   vpc-native-post-analytics-history-day-clickhouse-cron      kubernetes:batch/v1:CronJob    update
~   vpc-native-user-profile-analytics-clickhouse-cron          kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-zombie-opportunities-cron                 kubernetes:batch/v1:CronJob    update
~   vpc-native-hourly-notification-cron                        kubernetes:batch/v1:CronJob    update
~   vpc-native-update-trending-cron                            kubernetes:batch/v1:CronJob    update
-   vpc-native-api-clickhouse-migration-cbaddc6b               kubernetes:batch/v1:Job        delete
~   vpc-native-clean-zombie-user-companies-cron                kubernetes:batch/v1:CronJob    update
~   vpc-native-user-profile-updated-sync-cron                  kubernetes:batch/v1:CronJob    update
~   vpc-native-personalized-digest-deployment                  kubernetes:apps/v1:Deployment  update
~   vpc-native-deployment                                      kubernetes:apps/v1:Deployment  update
~   vpc-native-user-posts-analytics-refresh-cron               kubernetes:batch/v1:CronJob    update
~   vpc-native-temporal-deployment                             kubernetes:apps/v1:Deployment  update
+   vpc-native-api-db-migration-a0865745                       kubernetes:batch/v1:Job        create
~   vpc-native-bg-deployment                                   kubernetes:apps/v1:Deployment  update
~   vpc-native-clean-zombie-users-cron                         kubernetes:batch/v1:CronJob    update
~   vpc-native-clean-stale-user-transactions-cron              kubernetes:batch/v1:CronJob    update
~   vpc-native-ws-deployment                                   kubernetes:apps/v1:Deployment  update
~   vpc-native-clean-gifted-plus-cron                          kubernetes:batch/v1:CronJob    update
~   vpc-native-update-achievement-rarity-cron                  kubernetes:batch/v1:CronJob    update
-   vpc-native-api-db-migration-cbaddc6b                       kubernetes:batch/v1:Job        delete
~   vpc-native-personalized-digest-cron                        kubernetes:batch/v1:CronJob    update
... and 5 other changes

@nimrodkra
Copy link
Member Author

@claude please review this pr

@claude
Copy link

claude bot commented Mar 10, 2026

Claude finished @nimrodkra's task in 1m 43s —— View job


PR Review

  • Read changed files
  • Analyze code quality and correctness
  • Post review feedback

Summary: This is a clean, well-scoped copy and push heading change. No logic, data model, or migration changes. The dynamic push heading implementation is solid with proper fallback handling. A couple of minor notes below.


src/onesignal.ts — Dynamic Push Headings

The new pushHeadingFnMap + getPushHeading(type, title?) pattern is well-structured. The regex parsing falls back gracefully to static strings when there's no match.

One potential inconsistency to verify: The SquadPostAdded regex pattern:

title.match(/<b>([^<]+)<\/b>[^<]*<b>([^<]+)<\/b>/)

This works correctly against the title template <b>${ctx.doneBy.name}</b> shared a new post on <b>${ctx.source.name}</b>. However, if the squad name itself contains HTML entities (&amp;, &lt;, etc.), the match[2] extraction will surface raw HTML entity strings in the push heading rather than the decoded text. Worth checking if source names go through HTML escaping anywhere upstream.

Missing test coverage: The dynamic heading logic (pushHeadingFnMap) has no dedicated tests. The regex parsing is the only new code path in this PR that isn't exercised by the updated test assertions. A simple unit test verifying the heading output for each of the three dynamic types (and their fallbacks) would protect against regressions if title templates change in the future.

Fix this →


src/workers/notifications/utils.ts — UPVOTE_TITLES

Minor inconsistency: the 3 milestone uses a period while all others use !:

3: '<span class="text-theme-color-avocado">3 upvotes.</span> No bugs, just vibes ✨',
//                                                     ^
// All other entries use ! here (1, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000)

Fix this →


src/notifications/generate.ts — Title Copy Changes

All three title changes look good:

  • user_given_top_reader: restoring "Great news!" is a straightforward revert of the CTR-damaging change
  • achievement_unlocked: "You earned" adds personal ownership — solid improvement
  • squad_member_joined: "Say hi!" is an effective CTA

Tests

The two updated assertions correctly reflect the copy changes. No issues there.


Overall: LGTM with the minor 3 upvotes. period fix and ideally a test for the new dynamic heading logic. The implementation is clean and the fallback-first design is good.

Change "3 upvotes." to "3 upvotes!" to match all other milestones.

Made-with: Cursor
@nimrodkra nimrodkra merged commit bcf8dfa into main Mar 10, 2026
10 checks passed
@nimrodkra nimrodkra deleted the nimrod/notification-ctr-recovery branch March 10, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants