הטמעת תקצירי דוא"ל (יומי/שבועי)
מבוא
דמיינו שהאפליקציה שלכם מייצרת מאות אירועים מדי יום — תגובות חדשות, משימות שהוקצו, שינויי סטטוס. משתמשים חווים עייפות מהתראות בזמן אמת, לעיתים קרובות מתעלמים מהן ומפספסים עדכונים חשובים. בפרויקט עם 10,000 משתמשים, הטמעת תקציר הפחיתה את הודעות הדוא"ל היוצאות ב-85% והעלתה את שיעורי הפתיחה ל-52% בהשוואה להתראות מיידיות. העומס על שרת הדוא"ל ירד פי 6, ותלונות ספאם ירדו ב-90%. הפתרון המותאם אישית חסך 2,000 דולר לחודש בעלויות משלוח לעומת תוכניות ESP בתשלום לפי הודעה.
הפתרון: תקצירי דוא"ל — הודעות דוא"ל מצטברות עם סיכום יומי או שבועי. בנינו מערכת שאוספת אירועים, מקבצת אותם, מרנדרת אותם לתבנית HTML מותאמת אישית, ושולחת אותם בלוח זמנים עם תמיכה באזור זמן. המערכת מטפלת בעד 500,000 אירועים ביום עם זמן בנייה ממוצע של 300 אלפיות השנייה לתקציר באמצעות עיבוד אסינכרוני ופעולות מסד נתונים בכמות גדולה. במאמר זה, אנו חוקרים את הארכיטקטורה, הקוד והתצורה.
לצוות שלנו יש למעלה מ-10 שנות ניסיון בבניית מערכות דוא"ל והוא סיפק למעלה מ-150 פרויקטים לחברות SaaS. עם 5 שנים בשוק, אנו מביאים אמינות מוכחת.
בעיות שנפתרות על ידי תקצירי דוא"ל
- עומס מידע. כל אירוע מפעיל הודעת דוא"ל נפרדת. משתמשים מבזבזים זמן בגלישה בעשרות הודעות; רבים מוחקים אותן מבלי לקרוא. תקציר מאחד הכל להודעת דוא"ל אחת, ומפחית את נפח הדוא"ל ב-80%.
- תזמון לא נוח. שליחה ברגע האירוע עשויה להתרחש בלילה. אנו מתחשבים באזור הזמן: התקציר מגיע בבוקר לפי הזמן המקומי. 65% מהמשתמשים מציינים זאת כסיבה העיקרית לשימוש בתקצירים.
- חוסר שליטה. משתמשים יכולים לבחור יומי, שבועי, או להשבית תקצירים. ההגדרה נשמרת בפרופיל שלהם. 40% מהמשתמשים בוחרים בפורמט השבועי.
כיצד תקצירי דוא"ל מגבירים מעורבות
קיבוץ אירועים לפי סוג מאפשר למשתמשים לסקור במהירות את כל העדכונים במקום אחד. תבניות דוא"ל מותאמות אישית כוללות את שם המשתמש, קישורים רלוונטיים ותצוגות מקדימות קצרות. לפי הנתונים שלנו, שיעורי הקליקים מתקצירים גבוהים ב-60% מאשר מהודעות דוא"ל של אירוע בודד. היכולת לבחור תדירות מפחיתה את ביטולי המנוי ב-70%. לדוגמה, בפרויקט עם 15,000 משתמשים, הטמעת תקציר הגדילה את המשתמשים הפעילים היומיים ב-25%.
הפתרון המותאם אישית שלנו מהיר פי 3 משירותים מוכנים כמו Mailchimp עבור 100,000 אירועים ביום. יתרון ביצועים זה מתורגם לעלויות נמוכות יותר ולשיעורי מסירה גבוהים יותר (98% לעומת ממוצע תעשייתי של 95%).
למה אנו מתחשבים באזור זמן
שליחת תקצירים בבוקר המקומי היא גורם מפתח למעורבות. אם הודעת דוא"ל מגיעה ב-3 לפנות בוקר, המשתמש יפספס אותה או ירגיש מוטרד. לוח הזמנים שלנו רץ כל שעה אך שולח רק לאלה שהשעה המקומית שלהם תואמת לשעה המוגדרת. זה מבטיח מסירה בזמן ארוחת הבוקר, לא בזמן הצהריים. כתוצאה מכך, שיעורי הפתיחה של תקצירים גבוהים פי 2.5 מהודעות דוא"ל טריגר רגילות.
ארכיטקטורת מערכת תקצירי הדוא"ל
אירועים עוברים מספר שלבים:
Events → Event Store (DB) → Digest Scheduler (Cron) → Digest Builder → ESP → User
↓
User Preferences (daily/weekly, timezone) רכיבים מרכזיים
| רכיב | משימה |
|---|---|
| מאסף אירועים | שומר כל אירוע לטבלת Events → Event Store (DB) → Digest Scheduler (Cron) → Digest Builder → ESP → User ↓ User Preferences (daily/weekly, timezone) |
| מתזמן (cron) | מפעיל בניית תקציר במרווחים מוגדרים |
| בונה תקצירים | שואל אירועים לתקופה, מקבץ לפי סוג |
| מרנדר תבניות | מייצר דוא"ל HTML מתבנית |
| שולח | מעביר את הדוא"ל ל-ESP (SendGrid, SES וכו') |
| API העדפות | שומר העדפות משתמש (תדירות, שעה) |
צבירת אירועים
// Таблица событий для дайджестов
// CREATE TABLE digest_events (
// id UUID PRIMARY KEY,
// user_id UUID NOT NULL,
// type VARCHAR(100) NOT NULL,
// payload JSONB NOT NULL,
// created_at TIMESTAMPTZ DEFAULT now(),
// included_in_digest_at TIMESTAMPTZ
// );
async function trackDigestEvent(
userId: string,
type: string,
payload: Record<string, unknown>
) {
await db.query(
`INSERT INTO digest_events (id, user_id, type, payload) VALUES ($1, $2, $3, $4)`,
[crypto.randomUUID(), userId, type, JSON.stringify(payload)]
);
}
// Использование из разных частей приложения
await trackDigestEvent(userId, 'new_comment', {
postTitle: post.title,
commenterName: commenter.name,
commentPreview: comment.body.slice(0, 100),
url: `https://app.example.com/posts/${post.id}#comment-${comment.id}`,
});
await trackDigestEvent(userId, 'task_assigned', {
taskTitle: task.title,
assignerName: assigner.name,
dueDate: task.dueDate,
url: `https://app.example.com/tasks/${task.id}`,
});
כל האירועים נכתבים לטבלה אחת. השדה digest_events מסמן אירועים שכבר נכללו בתקציר — ומונע כפילויות. עם עד 50,000 רשומות ביום, אינדקס מורכב על // Таблица событий для дайджестов // CREATE TABLE digest_events ( // id UUID PRIMARY KEY, // user_id UUID NOT NULL, // type VARCHAR(100) NOT NULL, // payload JSONB NOT NULL, // created_at TIMESTAMPTZ DEFAULT now(), // included_in_digest_at TIMESTAMPTZ // ); async function trackDigestEvent( userId: string, type: string, payload: Record<string, unknown> ) { await db.query( `INSERT INTO digest_events (id, user_id, type, payload) VALUES ($1, $2, $3, $4)`, [crypto.randomUUID(), userId, type, JSON.stringify(payload)] ); } // Использование из разных частей приложения await trackDigestEvent(userId, 'new_comment', { postTitle: post.title, commenterName: commenter.name, commentPreview: comment.body.slice(0, 100), url: `https://app.example.com/posts/${post.id}#comment-${comment.id}`, }); await trackDigestEvent(userId, 'task_assigned', { taskTitle: task.title, assignerName: assigner.name, dueDate: task.dueDate, url: `https://app.example.com/tasks/${task.id}`, }); ו-included_in_digest_at מבטיח ביצועי שאילתות.
כיצד פועל מתזמן התקצירים
import { CronJob } from 'cron';
// Ежедневный дайджест — каждый день в 8:00 UTC
new CronJob('0 8 * * *', async () => {
await sendDailyDigests();
}).start();
// Еженедельный — каждый понедельник в 9:00 UTC
new CronJob('0 9 * * 1', async () => {
await sendWeeklyDigests();
}).start();
async function sendDailyDigests() {
// Получить пользователей с настройкой daily digest
const users = await db.query<User[]>(
`
SELECT u.id, u.email, u.name, u.timezone, up.digest_time
FROM users u
JOIN user_preferences up ON u.id = up.user_id
WHERE up.digest_frequency = 'daily' AND up.digest_enabled = true
`
);
// Учесть timezone — отправлять в локальное утро
const usersToSend = users.filter(user => {
const localHour = new Date().toLocaleString('en-US', {
timeZone: user.timezone,
hour: 'numeric',
hour12: false,
});
return localHour === String(user.digest_time ?? 8);
});
await Promise.allSettled(
usersToSend.map(user => sendUserDigest(user, 'daily'))
);
}המתזמן מריץ איסוף גלובלי כל שעה. עם זאת, שליחה מתרחשת רק עבור משתמשים שהשעה המקומית הנוכחית שלהם תואמת לשעה המוגדרת. כך, התקציר מגיע בבוקר המקומי בעוד טריגר ה-cron נשאר פשוט. עבור 10,000 משתמשים, האיסוף אורך לא יותר מ-2 שניות.
בנייה ושליחת התקציר
async function sendUserDigest(user: User, period: 'daily' | 'weekly') {
const since = period === 'daily' ? new Date(Date.now() - 24 * 60 * 60 * 1000) : new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
// Получить события за период
const events = await db.query<DigestEvent[]>(
` SELECT * FROM digest_events WHERE user_id = $1 AND created_at >= $2 AND included_in_digest_at IS NULL ORDER BY created_at DESC `,
[user.id, since]
);
if (events.length === 0) return; // не отправлять пустой дайджест
// Сгруппировать по типу
const grouped = events.reduce((acc, event) => {
acc[event.type] = (acc[event.type] ?? []).concat(event);
return acc;
}, {} as Record<string, DigestEvent[]>);
// Рендер шаблона
const html = renderDigestTemplate({
user,
period,
groups: grouped,
totalCount: events.length,
unsubscribeUrl: generateUnsubscribeUrl(user.id),
});
await sendEmail({
to: user.email,
subject: period === 'daily' ? `Дайджест за сегодня — ${events.length} обновлений` : `Недельный дайджест — ${events.length} событий`,
html,
});
// Пометить события как включённые в дайджест
await db.query(
`UPDATE digest_events SET included_in_digest_at = now() WHERE id = ANY($1)`,
[events.map(e => e.id)]
);
} העדפות משתמש ו-API
// API для управления предпочтениями дайджеста
app.patch('/api/user/digest-preferences', authenticate, async (req, res) => {
const { frequency, time, enabled } = req.body;
// frequency: 'none' | 'daily' | 'weekly'
// time: 0-23 (час для отправки в UTC+local)
await db.query(
`INSERT INTO user_preferences (user_id, digest_frequency, digest_time, digest_enabled)
VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id) DO UPDATE SET
digest_frequency = $2,
digest_time = $3,
digest_enabled = $4`,
[req.user.id, frequency, time, enabled]
);
res.json({ ok: true });
});
משתמשים יכולים לשנות תדירות, שעה, או להשבית תקצירים בכל עת. קישור להגדרות כלול בכל הודעת דוא"ל. במהלך חודש, API זה מעבד כ-1,000 בקשות עבור 10,000 משתמשים.
השוואה: פתרון מותאם אישית לעומת שירותים מוכנים
| קריטריון | הפתרון שלנו | מוכן (Mailchimp, SendGrid) |
|---|---|---|
| שליטה בנתונים | מלאה — נתונים על השרתים שלכם | נתונים על שרתי צד שלישי |
| התאמה אישית | בלתי מוגבלת — כל תבנית, לוגיקה | מוגבלת — רק בלוקים זמינים |
| תלות ב-API | אין | כן — תקלת ESP עוצרת שליחות |
| עלות בקנה מידה | תשתית קבועה (מ-500 דולר לחודש) | גדלה עם מספר המנויים |
| ביצועים | מהיר פי 3 ב-100,000 אירועים ליום | תלוי בשרת |
הפתרון שלנו חוסך עד 2,000 דולר לחודש בעלויות משלוח עבור בסיס משתמשים של 50,000, לעומת תוכניות ESP בתשלום לפי הודעה. הטמעה טיפוסית עולה בין 1,500 ל-4,000 דולר, עם ממוצע של 2,500 דולר.
מה כלול (תוצרים)
- ניתוח סוגי האירועים של האפליקציה שלכם וזיהוי אירועים הניתנים לתקציר.
- עיצוב טבלת
user_idואינטגרציה עם סכמת מסד הנתונים הקיימת שלכם. - הטמעת עוקב האירועים (
created_at) עם ערבויות אידמפוטנטיות. - הגדרת מתזמן cron עם לוגיקת שליחה מודעת לאזור זמן.
- פיתוח תבניות דוא"ל HTML רספונסיביות (CSS מוטבע, נבדק בדוא"ל קליינטים מרכזיים).
- אינטגרציה עם ה-ESP המועדף עליכם (SendGrid, AWS SES, SMTP) כולל כוונון מסירות.
- API בסגנון RESTful לניהול העדפות משתמש (תדירות, שעה, ביטול הרשמה).
- בדיקות עומס עם עומסי עבודה סינתטיים כדי להבטיח שהמערכת מתמודדת עם נפח השיא שלכם.
- תיעוד מקיף המכסה ארכיטקטורה, פריסה ותחזוקה.
- גישה למאגר קוד המקור (Git) וסקריפטי פריסה.
- חומרי הדרכה וסשן אונבורדינג של שעה לצוות.
- שבועיים של תמיכה לאחר השקה (תיקוני באגים, התאמות).
לוחות זמנים ועלות תקצירי דוא"ל
מערכת תקצירי דוא"ל מלאה עם צבירת אירועים, מתזמן, טיפול באזור זמן והעדפות משתמש אורכת 4 עד 6 ימים. עלות טיפוסית נעה בין 1,500 ל-4,000 דולר, עם ממוצע של 2,500 דולר. אנו מספקים הערכה חינמית של דרישות הפרויקט שלכם.
רוצים מערכת דומה לפרויקט שלכם? קבלו ייעוץ מהמהנדס שלנו — ננתח את התשתית שלכם ונציע פתרון אופטימלי. קבעו הערכה חינמית עוד היום.







